├── .github └── workflows │ ├── build.yml │ └── docker.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── _ext │ └── linkcode_res.py ├── _static │ └── .keep ├── _templates │ └── .keep ├── conf.py ├── index.rst ├── make.bat └── source │ ├── modules.rst │ ├── xbox.rst │ ├── xbox.webapi.api.client.rst │ ├── xbox.webapi.api.language.rst │ ├── xbox.webapi.api.provider.account.models.rst │ ├── xbox.webapi.api.provider.account.rst │ ├── xbox.webapi.api.provider.achievements.models.rst │ ├── xbox.webapi.api.provider.achievements.rst │ ├── xbox.webapi.api.provider.baseprovider.rst │ ├── xbox.webapi.api.provider.catalog.const.rst │ ├── xbox.webapi.api.provider.catalog.models.rst │ ├── xbox.webapi.api.provider.catalog.rst │ ├── xbox.webapi.api.provider.cqs.models.rst │ ├── xbox.webapi.api.provider.cqs.rst │ ├── xbox.webapi.api.provider.gameclips.models.rst │ ├── xbox.webapi.api.provider.gameclips.rst │ ├── xbox.webapi.api.provider.lists.models.rst │ ├── xbox.webapi.api.provider.lists.rst │ ├── xbox.webapi.api.provider.mediahub.models.rst │ ├── xbox.webapi.api.provider.mediahub.rst │ ├── xbox.webapi.api.provider.message.models.rst │ ├── xbox.webapi.api.provider.message.rst │ ├── xbox.webapi.api.provider.people.models.rst │ ├── xbox.webapi.api.provider.people.rst │ ├── xbox.webapi.api.provider.presence.models.rst │ ├── xbox.webapi.api.provider.presence.rst │ ├── xbox.webapi.api.provider.profile.models.rst │ ├── xbox.webapi.api.provider.profile.rst │ ├── xbox.webapi.api.provider.rst │ ├── xbox.webapi.api.provider.screenshots.models.rst │ ├── xbox.webapi.api.provider.screenshots.rst │ ├── xbox.webapi.api.provider.smartglass.models.rst │ ├── xbox.webapi.api.provider.smartglass.rst │ ├── xbox.webapi.api.provider.titlehub.models.rst │ ├── xbox.webapi.api.provider.titlehub.rst │ ├── xbox.webapi.api.provider.usersearch.models.rst │ ├── xbox.webapi.api.provider.usersearch.rst │ ├── xbox.webapi.api.provider.userstats.models.rst │ ├── xbox.webapi.api.provider.userstats.rst │ ├── xbox.webapi.api.rst │ ├── xbox.webapi.authentication.manager.rst │ ├── xbox.webapi.authentication.models.rst │ ├── xbox.webapi.authentication.rst │ ├── xbox.webapi.authentication.xal.rst │ ├── xbox.webapi.common.exceptions.rst │ ├── xbox.webapi.common.filetimes.rst │ ├── xbox.webapi.common.models.rst │ ├── xbox.webapi.common.request_signer.rst │ ├── xbox.webapi.common.rst │ ├── xbox.webapi.common.signed_session.rst │ ├── xbox.webapi.rst │ ├── xbox.webapi.scripts.authenticate.rst │ ├── xbox.webapi.scripts.change_gamertag.rst │ ├── xbox.webapi.scripts.friends.rst │ ├── xbox.webapi.scripts.rst │ ├── xbox.webapi.scripts.search.rst │ └── xbox.webapi.scripts.xal.rst ├── pyproject.toml ├── readme_example.py ├── setup.cfg ├── tests ├── __init__.py ├── common.py ├── conftest.py ├── data │ ├── responses │ │ ├── achievements_360_all.json │ │ ├── achievements_360_earned.json │ │ ├── achievements_360_recent_progress.json │ │ ├── achievements_one_details.json │ │ ├── achievements_one_gameprogress.json │ │ ├── achievements_one_recent_progress.json │ │ ├── auth_device_token.json │ │ ├── auth_oauth2_token.json │ │ ├── auth_title_endpoints.json │ │ ├── auth_user_token.json │ │ ├── auth_xsts_token.json │ │ ├── catalog_browse.json │ │ ├── catalog_browse_details.json │ │ ├── catalog_product_lookup.json │ │ ├── catalog_product_lookup_legacy.json │ │ ├── catalog_search.json │ │ ├── cqs_get_channel_list.json │ │ ├── cqs_get_schedule.json │ │ ├── gameclips_recent_community.json │ │ ├── gameclips_recent_own.json │ │ ├── gameclips_recent_own_titleid.json │ │ ├── gameclips_recent_xuid.json │ │ ├── gameclips_recent_xuid_titleid.json │ │ ├── gameclips_saved_community.json │ │ ├── gameclips_saved_own.json │ │ ├── gameclips_saved_own_titleid.json │ │ ├── gameclips_saved_xuid.json │ │ ├── gameclips_saved_xuid_titleid.json │ │ ├── list_add_item.json │ │ ├── list_delete_item.json │ │ ├── lists_get_items.json │ │ ├── mediahub_gameclips_own.json │ │ ├── mediahub_screenshots_own.json │ │ ├── message_get_conversation.json │ │ ├── message_get_inbox.json │ │ ├── message_new_conversation.json │ │ ├── message_send_message.json │ │ ├── people_batch.json │ │ ├── people_friends_by_xuid.json │ │ ├── people_friends_own.json │ │ ├── people_recommendations.json │ │ ├── people_summary_by_gamertag.json │ │ ├── people_summary_by_xuid.json │ │ ├── people_summary_own.json │ │ ├── presence.json │ │ ├── presence_batch.json │ │ ├── presence_own.json │ │ ├── profile_batch.json │ │ ├── profile_by_gamertag.json │ │ ├── profile_by_xuid.json │ │ ├── screenshots_recent_community.json │ │ ├── screenshots_recent_own.json │ │ ├── screenshots_recent_own_titleid.json │ │ ├── screenshots_recent_xuid.json │ │ ├── screenshots_recent_xuid_titleid.json │ │ ├── screenshots_saved_community.json │ │ ├── screenshots_saved_own.json │ │ ├── screenshots_saved_own_titleid.json │ │ ├── screenshots_saved_xuid.json │ │ ├── screenshots_saved_xuid_titleid.json │ │ ├── smartglass_command.json │ │ ├── smartglass_console_list.json │ │ ├── smartglass_console_status.json │ │ ├── smartglass_installed_apps.json │ │ ├── smartglass_op_status.json │ │ ├── smartglass_storage_devices.json │ │ ├── titlehub_batch.json │ │ ├── titlehub_titlehistory.json │ │ ├── titlehub_titleinfo.json │ │ ├── usersearch_live_search.json │ │ ├── userstats_batch.json │ │ ├── userstats_batch_by_scid.json │ │ ├── userstats_by_scid.json │ │ ├── userstats_by_scid_with_metadata.json │ │ ├── xal_authentication_resp.json │ │ └── xal_authorization_resp.json │ └── test_signing_key.pem ├── test_account.py ├── test_achievements.py ├── test_auth.py ├── test_catalog.py ├── test_cqs.py ├── test_gameclips.py ├── test_lists.py ├── test_mediahub.py ├── test_message.py ├── test_people.py ├── test_presence.py ├── test_profile.py ├── test_ratelimits.py ├── test_request_signer.py ├── test_screenshots.py ├── test_signed_session.py ├── test_smartglass.py ├── test_titlehub.py ├── test_usersearch.py ├── test_userstats.py ├── test_xal.py └── test_xbl_client.py └── xbox └── webapi ├── __init__.py ├── api ├── __init__.py ├── client.py ├── language.py └── provider │ ├── __init__.py │ ├── account │ ├── __init__.py │ └── models.py │ ├── achievements │ ├── __init__.py │ └── models.py │ ├── baseprovider.py │ ├── catalog │ ├── __init__.py │ ├── const.py │ └── models.py │ ├── cqs │ ├── __init__.py │ └── models.py │ ├── gameclips │ ├── __init__.py │ └── models.py │ ├── lists │ ├── __init__.py │ └── models.py │ ├── mediahub │ ├── __init__.py │ └── models.py │ ├── message │ ├── __init__.py │ └── models.py │ ├── people │ ├── __init__.py │ └── models.py │ ├── presence │ ├── __init__.py │ └── models.py │ ├── profile │ ├── __init__.py │ └── models.py │ ├── ratelimitedprovider.py │ ├── screenshots │ ├── __init__.py │ └── models.py │ ├── smartglass │ ├── __init__.py │ └── models.py │ ├── titlehub │ ├── __init__.py │ └── models.py │ ├── usersearch │ ├── __init__.py │ └── models.py │ └── userstats │ ├── __init__.py │ └── models.py ├── authentication ├── __init__.py ├── manager.py ├── models.py └── xal.py ├── common ├── __init__.py ├── exceptions.py ├── filetimes.py ├── models.py ├── ratelimits │ ├── __init__.py │ └── models.py ├── request_signer.py └── signed_session.py └── scripts ├── __init__.py ├── authenticate.py ├── change_gamertag.py ├── friends.py ├── search.py └── xal.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -e .[dev] 23 | - name: Lint with ruff 24 | run: | 25 | ruff check xbox 26 | ruff check tests 27 | - name: Test with pytest 28 | run: | 29 | pytest 30 | 31 | deploy: 32 | runs-on: ubuntu-latest 33 | needs: build 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Set up Python 37 | uses: actions/setup-python@v4 38 | with: 39 | python-version: '3.12' 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install setuptools wheel twine build 44 | - name: Build and publish 45 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 46 | env: 47 | TWINE_USERNAME: __token__ 48 | TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }} 49 | run: | 50 | python -m build 51 | twine upload dist/* 52 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | pull_request: 5 | branches: master 6 | push: 7 | branches: master 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | buildx: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | - 19 | name: Prepare 20 | id: prepare 21 | run: | 22 | DOCKER_IMAGE=openxbox/xbox-webapi-python 23 | DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64 24 | VERSION=edge 25 | 26 | if [[ $GITHUB_REF == refs/tags/* ]]; then 27 | VERSION=${GITHUB_REF#refs/tags/v} 28 | fi 29 | if [ "${{ github.event_name }}" = "schedule" ]; then 30 | VERSION=nightly 31 | fi 32 | 33 | TAGS="--tag ${DOCKER_IMAGE}:${VERSION}" 34 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 35 | TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest" 36 | fi 37 | 38 | echo ::set-output name=docker_image::${DOCKER_IMAGE} 39 | echo ::set-output name=version::${VERSION} 40 | echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \ 41 | --build-arg VERSION=${VERSION} \ 42 | --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ 43 | --build-arg VCS_REF=${GITHUB_SHA::8} \ 44 | ${TAGS} --file ./Dockerfile . 45 | - 46 | name: Set up QEMU 47 | uses: docker/setup-qemu-action@v1 48 | with: 49 | platforms: all 50 | - 51 | name: Set up Docker Buildx 52 | id: buildx 53 | uses: docker/setup-buildx-action@v1 54 | with: 55 | version: latest 56 | - 57 | name: Available platforms 58 | run: echo ${{ steps.buildx.outputs.platforms }} 59 | - 60 | name: Docker Buildx (build) 61 | run: | 62 | docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }} 63 | - 64 | name: Login to DockerHub 65 | if: success() && github.event_name != 'pull_request' 66 | uses: docker/login-action@v1 67 | with: 68 | username: ${{ secrets.DOCKER_USERNAME }} 69 | password: ${{ secrets.DOCKER_PASSWORD }} 70 | - 71 | name: Docker Buildx (push) 72 | if: success() && github.event_name != 'pull_request' 73 | run: | 74 | docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} 75 | - 76 | name: Update Dockerhub description 77 | if: success() && github.event_name != 'pull_request' 78 | uses: peter-evans/dockerhub-description@v2 79 | env: 80 | DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} 81 | DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 82 | DOCKERHUB_REPOSITORY: ${{ steps.prepare.outputs.docker_image }} 83 | README_FILEPATH: ./README.md 84 | - 85 | name: Inspect image 86 | if: always() && github.event_name != 'pull_request' 87 | run: | 88 | docker buildx imagetools inspect ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | .env 12 | .venv/ 13 | venv/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | #Virtualenv folders and files 30 | Scripts 31 | pyvenv.cfg 32 | Lib 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | .hypothesis/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # Intellij 69 | .idea/ 70 | 71 | # VS Code 72 | .vscode/ 73 | 74 | # Node 75 | node_modules 76 | 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.1.6 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [ --fix ] 9 | # Run the formatter. 10 | - id: ruff-format 11 | - repo: https://github.com/asottile/pyupgrade 12 | rev: v3.15.0 13 | hooks: 14 | - id: pyupgrade 15 | args: [--py38-plus] 16 | - repo: https://github.com/psf/black 17 | rev: 23.11.0 18 | hooks: 19 | - id: black 20 | args: 21 | - --safe 22 | - --quiet 23 | files: ^((xbox|tests)/.+)?[^/]+\.py$ 24 | - repo: https://github.com/PyCQA/bandit 25 | rev: 1.7.5 26 | hooks: 27 | - id: bandit 28 | args: 29 | - --configfile=pyproject.toml 30 | - --quiet 31 | - --format=custom 32 | files: ^(xbox|tests)/.+\.py$ 33 | - repo: https://github.com/PyCQA/isort 34 | rev: 5.12.0 35 | hooks: 36 | - id: isort 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: v4.5.0 39 | hooks: 40 | - id: check-executables-have-shebangs 41 | stages: [manual] 42 | - id: check-json -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html#supported-settings 2 | 3 | version: 2 4 | 5 | sphinx: 6 | # The config file overrides the UI settings: 7 | # https://github.com/pyca/cryptography/issues/5863#issuecomment-817828152 8 | builder: dirhtml 9 | 10 | build: 11 | # readdocs master now includes a rust toolchain 12 | os: "ubuntu-22.04" 13 | tools: 14 | python: "3.12" 15 | 16 | python: 17 | install: 18 | - method: pip 19 | path: . 20 | extra_requirements: 21 | - docs 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | Contributions are welcome, and they are greatly appreciated! Every 5 | little bit helps, and credit will always be given. 6 | 7 | You can contribute in many ways: 8 | 9 | ## Types of Contributions 10 | 11 | ### Report Bugs 12 | 13 | Report bugs at . 14 | 15 | If you are reporting a bug, please include: 16 | 17 | * Your operating system name and version. 18 | * Any details about your local setup that might be helpful in troubleshooting. 19 | * Detailed steps to reproduce the bug. 20 | 21 | ### Fix Bugs 22 | 23 | Look through the GitHub issues for bugs. Anything tagged with "bug" 24 | is open to whoever wants to implement it. 25 | 26 | ### Implement Features 27 | 28 | Look through the GitHub issues for features. Anything tagged with "feature" 29 | is open to whoever wants to implement it. 30 | 31 | ### Write Documentation 32 | 33 | xbox-webapi-python could always use more documentation, whether as part of the 34 | official xbox-webapi-python docs, in docstrings, or even on the web in blog posts, 35 | articles, and such. 36 | 37 | ### Submit Feedback 38 | 39 | The best way to send feedback is to file an issue at . 40 | 41 | If you are proposing a feature: 42 | 43 | * Explain in detail how it would work. 44 | * Keep the scope as narrow as possible, to make it easier to implement. 45 | * Remember that this is a volunteer-driven project, and that contributions 46 | are welcome :) 47 | 48 | ### Get Started 49 | 50 | Ready to contribute? Here's how to set up `xbox-webapi-python` for local development. 51 | 52 | 1. Fork the `xbox-webapi-python` repo on GitHub. 53 | 2. Clone your fork locally 54 | 55 | ```text 56 | git clone git@github.com:your_name_here/xbox-webapi-python.git 57 | ``` 58 | 59 | 3. Install your local copy into a virtualenv. Assuming you have venv installed, this is how you set up your fork for local development 60 | 61 | ```text 62 | cd xbox-webapi-python 63 | python -m venv venv 64 | source venv/bin/activate 65 | pip install -e .[dev] 66 | pre-commit install 67 | ``` 68 | 69 | 4. Create a branch for local development:: 70 | 71 | ```text 72 | git checkout -b name-of-your-bugfix-or-feature 73 | ``` 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass the tests 78 | 79 | ```text 80 | pytest 81 | ``` 82 | 83 | 6. Commit your changes and push your branch to GitHub 84 | 85 | ``` 86 | git add . 87 | git commit -m "Your detailed description of your changes." 88 | git push origin name-of-your-bugfix-or-feature 89 | ``` 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | ### Pull Request Guidelines 94 | 95 | Before you submit a pull request, check that it meets these guidelines: 96 | 97 | 1. The pull request should include tests. 98 | 2. If the pull request adds functionality, the docs should be updated. Put 99 | your new functionality into a function with a docstring, and add the 100 | feature to the list in README.md. 101 | 3. The pull request should work for Python 3.8+. Check 102 | 103 | and make sure that the tests pass for all supported Python versions. 104 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Based on https://softwarejourneyman.com/docker-python-install-wheels.html 2 | 3 | ######################################### 4 | # Image WITH C compiler, building wheels for next stage 5 | FROM python:3.12-alpine as bigimage 6 | 7 | ENV LANG C.UTF-8 8 | 9 | # Copy project files 10 | COPY . /src/xbox-webapi 11 | 12 | # install the C compiler 13 | RUN apk add --no-cache jq gcc musl-dev libffi-dev openssl-dev cargo 14 | 15 | # instead of installing, create a wheel 16 | RUN pip wheel --wheel-dir=/root/wheels /src/xbox-webapi 17 | 18 | ######################################### 19 | # Image WITHOUT C compiler, installing the component from wheel 20 | FROM python:3.12-alpine as smallimage 21 | 22 | RUN apk add --no-cache openssl 23 | 24 | COPY --from=bigimage /root/wheels /root/wheels 25 | 26 | # Ignore the Python package index 27 | # and look for archives in 28 | # /root/wheels directory 29 | RUN pip install \ 30 | --no-index \ 31 | --find-links=/root/wheels \ 32 | xbox-webapi 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 OpenXbox 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md 2 | include CHANGELOG.md 3 | include LICENSE 4 | include README.md 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -fr {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -f .coverage 49 | rm -fr htmlcov/ 50 | 51 | lint: ## check style with ruff 52 | ruff check --fix xbox 53 | ruff check --fix tests 54 | 55 | test: ## run tests quickly with the default Python 56 | py.test 57 | 58 | coverage: ## check code coverage quickly with the default Python 59 | coverage run --source xbox -m pytest 60 | coverage report -m 61 | coverage html 62 | $(BROWSER) htmlcov/index.html 63 | 64 | docs: ## generate Sphinx HTML documentation, including API docs 65 | rm -f docs/xbox.rst 66 | rm -f docs/modules.rst 67 | sphinx-apidoc --implicit-namespaces -a -e -o docs/source xbox 68 | $(MAKE) -C docs clean 69 | $(MAKE) -C docs html 70 | $(BROWSER) docs/_build/html/index.html 71 | 72 | servedocs: docs ## compile the docs watching for changes 73 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 74 | 75 | release: clean ## package and upload a release 76 | twine upload dist/* 77 | 78 | dist: clean ## builds source and wheel package 79 | python -m build 80 | ls -l dist 81 | 82 | install: clean ## install the package to the active Python's site-packages 83 | pre-commit install 84 | pip install -e . 85 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = XboxWebAPI 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_ext/linkcode_res.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import os 4 | import sys 5 | 6 | import xbox.webapi 7 | 8 | # -- Linkcode resolver ----------------------------------------------------- 9 | 10 | # This is HEAVILY inspired by numpy's 11 | # https://github.com/numpy/numpy/blob/73fe877ff967f279d470b81ad447b9f3056c1335/doc/source/conf.py#L390 12 | 13 | # Copyright (c) 2005-2020, NumPy Developers. 14 | # All rights reserved. 15 | # 16 | # Redistribution and use in source and binary forms, with or without 17 | # modification, are permitted provided that the following conditions are 18 | # met: 19 | # 20 | # * Redistributions of source code must retain the above copyright 21 | # notice, this list of conditions and the following disclaimer. 22 | # 23 | # * Redistributions in binary form must reproduce the above 24 | # copyright notice, this list of conditions and the following 25 | # disclaimer in the documentation and/or other materials provided 26 | # with the distribution. 27 | # 28 | # * Neither the name of the NumPy Developers nor the names of any 29 | # contributors may be used to endorse or promote products derived 30 | # from this software without specific prior written permission. 31 | # 32 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 33 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 34 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 35 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 36 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 37 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 38 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 39 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 40 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 41 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 42 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 | 44 | 45 | def linkcode_resolve(domain, info): 46 | """ 47 | Determine the url corresponding to Python object 48 | """ 49 | if domain != "py": 50 | return None 51 | 52 | modname = info["module"] 53 | fullname = info["fullname"] 54 | 55 | try: 56 | importlib.import_module(modname) 57 | except Exception: 58 | return None 59 | submod = sys.modules.get(modname) 60 | if submod is None: 61 | return None 62 | 63 | obj = submod 64 | for part in fullname.split("."): 65 | try: 66 | obj = getattr(obj, part) 67 | except Exception: 68 | return None 69 | 70 | # strip decorators, which would resolve to the source of the decorator 71 | # possibly an upstream bug in getsourcefile, bpo-1764286 72 | try: 73 | unwrap = inspect.unwrap 74 | except AttributeError: 75 | pass 76 | else: 77 | obj = unwrap(obj) 78 | 79 | fn = None 80 | lineno = None 81 | 82 | try: 83 | fn = inspect.getsourcefile(obj) 84 | except Exception: 85 | fn = None 86 | if not fn: 87 | return None 88 | 89 | try: 90 | source, lineno = inspect.getsourcelines(obj) 91 | except Exception: 92 | lineno = None 93 | 94 | fn = os.path.relpath(fn, start=os.path.dirname(xbox.webapi.__file__)) 95 | 96 | if lineno: 97 | linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) 98 | else: 99 | linespec = "" 100 | 101 | url = "https://github.com/OpenXbox/xbox-webapi-python/blob/%s/xbox/webapi/%s%s" 102 | # url = "https://github.com/pyca/cryptography/blob/%s/src/cryptography/%s%s" 103 | if "dev" in xbox.webapi.__version__: 104 | return url % ("master", fn, linespec) 105 | else: 106 | version = f"v{xbox.webapi.__version__}" 107 | return url % (version, fn, linespec) -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/docs/_static/.keep -------------------------------------------------------------------------------- /docs/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/docs/_templates/.keep -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Xbox WebAPI documentation master file, created by 2 | sphinx-quickstart on Tue Mar 13 18:40:24 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Xbox WebAPI's documentation! 7 | ======================================= 8 | 9 | .. mdinclude:: ../README.md 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | source/xbox.webapi.authentication.manager 16 | source/xbox.webapi.api.client 17 | source/xbox.webapi.api.language 18 | source/xbox.webapi.api.provider 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=XboxWebAPI 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | xbox 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | xbox.webapi 8 | -------------------------------------------------------------------------------- /docs/source/xbox.rst: -------------------------------------------------------------------------------- 1 | xbox namespace 2 | ============== 3 | 4 | .. py:module:: xbox 5 | 6 | Subpackages 7 | ----------- 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | 12 | xbox.webapi 13 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.client.rst: -------------------------------------------------------------------------------- 1 | Xbox Live Client - HTTP Client wrapper 2 | ====================================== 3 | 4 | .. automodule:: xbox.webapi.api.client 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.language.rst: -------------------------------------------------------------------------------- 1 | Xbox Live language definitions 2 | ============================== 3 | 4 | .. automodule:: xbox.webapi.api.language 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.account.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.account.models module 2 | ============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.account.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.account.rst: -------------------------------------------------------------------------------- 1 | Acccount - Change your Gamertag 2 | =============================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.account 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.achievements.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.achievements.models module 2 | =================================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.achievements.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.achievements.rst: -------------------------------------------------------------------------------- 1 | Achievements - Get info about gameprogress 2 | ========================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.achievements 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.baseprovider.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.baseprovider module 2 | ============================================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.baseprovider 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.catalog.const.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.catalog.const module 2 | ============================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.catalog.const 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.catalog.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.catalog.models module 2 | ============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.catalog.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.catalog.rst: -------------------------------------------------------------------------------- 1 | Catalog - Microsoft Store Catalog 2 | ================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.catalog 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.cqs.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.cqs.models module 2 | ========================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.cqs.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.cqs.rst: -------------------------------------------------------------------------------- 1 | CQS - Stump TV Streaming 2 | ====================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.cqs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.gameclips.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.gameclips.models module 2 | ================================================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.gameclips.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.gameclips.rst: -------------------------------------------------------------------------------- 1 | Gameclips - Own, from Community, by XUID 2 | ======================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.gameclips 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.lists.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.lists.models module 2 | ============================================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.lists.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.lists.rst: -------------------------------------------------------------------------------- 1 | EPLists - Manage Xbox Live Pins 2 | =============================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.lists 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.mediahub.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.mediahub.models module 2 | =============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.mediahub.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.mediahub.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.mediahub package 2 | ========================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | xbox.webapi.api.provider.mediahub.models 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: xbox.webapi.api.provider.mediahub 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.message.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.message.models module 2 | ============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.message.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.message.rst: -------------------------------------------------------------------------------- 1 | Message - Read and send messages 2 | ================================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.message 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.people.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.people.models module 2 | ============================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.people.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.people.rst: -------------------------------------------------------------------------------- 1 | People - Get friendlist info 2 | ============================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.people 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.presence.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.presence.models module 2 | =============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.presence.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.presence.rst: -------------------------------------------------------------------------------- 1 | Presence - Get online status of friends 2 | ======================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.presence 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.profile.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.profile.models module 2 | ============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.profile.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.profile.rst: -------------------------------------------------------------------------------- 1 | Profile - Get Userprofile information 2 | ===================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.profile 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.rst: -------------------------------------------------------------------------------- 1 | Xbox Live Providers - API Endpoints 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.api.provider.account 10 | xbox.webapi.api.provider.catalog 11 | xbox.webapi.api.provider.cqs 12 | xbox.webapi.api.provider.lists 13 | xbox.webapi.api.provider.profile 14 | xbox.webapi.api.provider.achievements 15 | xbox.webapi.api.provider.usersearch 16 | xbox.webapi.api.provider.gameclips 17 | xbox.webapi.api.provider.people 18 | xbox.webapi.api.provider.presence 19 | xbox.webapi.api.provider.message 20 | xbox.webapi.api.provider.userstats 21 | xbox.webapi.api.provider.screenshots 22 | xbox.webapi.api.provider.titlehub 23 | xbox.webapi.api.provider.smartglass 24 | 25 | Module contents 26 | --------------- 27 | 28 | .. automodule:: xbox.webapi.api.provider 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.screenshots.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.screenshots.models module 2 | ================================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.screenshots.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.screenshots.rst: -------------------------------------------------------------------------------- 1 | Screenshots - Get screenshot info 2 | ================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.screenshots 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.smartglass.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.smartglass.models module 2 | ================================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.smartglass.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.smartglass.rst: -------------------------------------------------------------------------------- 1 | Smartglass - Control your Xbox 2 | ============================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.smartglass 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.titlehub.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.titlehub.models module 2 | =============================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.titlehub.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.titlehub.rst: -------------------------------------------------------------------------------- 1 | Titlehub - Get Title history and info 2 | ===================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.titlehub 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.usersearch.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.usersearch.models module 2 | ================================================= 3 | 4 | .. automodule:: xbox.webapi.api.provider.usersearch.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.usersearch.rst: -------------------------------------------------------------------------------- 1 | Usersearch - Search users / gamertags 2 | ===================================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.usersearch 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.userstats.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api.provider.userstats.models module 2 | ================================================ 3 | 4 | .. automodule:: xbox.webapi.api.provider.userstats.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.provider.userstats.rst: -------------------------------------------------------------------------------- 1 | Userstats - Get game statistics 2 | =============================== 3 | 4 | .. automodule:: xbox.webapi.api.provider.userstats 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.api.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.api package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.api.client 10 | xbox.webapi.api.language 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: xbox.webapi.api 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.authentication.manager.rst: -------------------------------------------------------------------------------- 1 | Authentication Manager - Authenticate with MS / XBL 2 | =================================================== 3 | 4 | .. automodule:: xbox.webapi.authentication.manager 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.authentication.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.authentication.models module 2 | ======================================== 3 | 4 | .. automodule:: xbox.webapi.authentication.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.authentication.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.authentication package 2 | ================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.authentication.manager 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: xbox.webapi.authentication 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.authentication.xal.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.authentication.xal module 2 | ===================================== 3 | 4 | .. automodule:: xbox.webapi.authentication.xal 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.exceptions.rst: -------------------------------------------------------------------------------- 1 | Custom Exceptions 2 | ================= 3 | 4 | .. automodule:: xbox.webapi.common.exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.filetimes.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.common.filetimes module 2 | =================================== 3 | 4 | .. automodule:: xbox.webapi.common.filetimes 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.models.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.common.models module 2 | ================================ 3 | 4 | .. automodule:: xbox.webapi.common.models 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.request_signer.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.common.request\_signer module 2 | ========================================= 3 | 4 | .. automodule:: xbox.webapi.common.request_signer 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.common package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.common.exceptions 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: xbox.webapi.common 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.common.signed_session.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.common.signed\_session module 2 | ========================================= 3 | 4 | .. automodule:: xbox.webapi.common.signed_session 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.scripts 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: xbox.webapi 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.authenticate.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts.authenticate module 2 | ======================================= 3 | 4 | .. automodule:: xbox.webapi.scripts.authenticate 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.change_gamertag.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts.change\_gamertag module 2 | =========================================== 3 | 4 | .. automodule:: xbox.webapi.scripts.change_gamertag 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.friends.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts.friends module 2 | ================================== 3 | 4 | .. automodule:: xbox.webapi.scripts.friends 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts namespace 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.webapi.scripts.authenticate 10 | xbox.webapi.scripts.search 11 | 12 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.search.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts.search module 2 | ================================= 3 | 4 | .. automodule:: xbox.webapi.scripts.search 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.webapi.scripts.xal.rst: -------------------------------------------------------------------------------- 1 | xbox.webapi.scripts.xal module 2 | ============================== 3 | 4 | .. automodule:: xbox.webapi.scripts.xal 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /readme_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from httpx import HTTPStatusError 5 | 6 | from xbox.webapi.api.client import XboxLiveClient 7 | from xbox.webapi.authentication.manager import AuthenticationManager 8 | from xbox.webapi.authentication.models import OAuth2TokenResponse 9 | from xbox.webapi.common.signed_session import SignedSession 10 | from xbox.webapi.scripts import CLIENT_ID, CLIENT_SECRET, TOKENS_FILE 11 | 12 | """ 13 | This uses the global default client identification by OpenXbox 14 | You can supply your own parameters here if you are permitted to create 15 | new Microsoft OAuth Apps and know what you are doing 16 | """ 17 | client_id = CLIENT_ID 18 | client_secret = CLIENT_SECRET 19 | tokens_file = TOKENS_FILE 20 | 21 | """ 22 | For doing authentication, see xbox/webapi/scripts/authenticate.py 23 | """ 24 | 25 | 26 | async def async_main(): 27 | # Create a HTTP client session 28 | async with SignedSession() as session: 29 | """ 30 | Initialize with global OAUTH parameters from above 31 | """ 32 | auth_mgr = AuthenticationManager(session, client_id, client_secret, "") 33 | 34 | """ 35 | Read in tokens that you received from the `xbox-authenticate`-script previously 36 | See `xbox/webapi/scripts/authenticate.py` 37 | """ 38 | try: 39 | with open(tokens_file) as f: 40 | tokens = f.read() 41 | # Assign gathered tokens 42 | auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens) 43 | except FileNotFoundError as e: 44 | print( 45 | f"File {tokens_file} isn`t found or it doesn`t contain tokens! err={e}" 46 | ) 47 | print("Authorizing via OAUTH") 48 | url = auth_mgr.generate_authorization_url() 49 | print(f"Auth via URL: {url}") 50 | authorization_code = input("Enter authorization code> ") 51 | tokens = await auth_mgr.request_oauth_token(authorization_code) 52 | auth_mgr.oauth = tokens 53 | 54 | """ 55 | Refresh tokens, just in case 56 | You could also manually check the token lifetimes and just refresh them 57 | if they are close to expiry 58 | """ 59 | try: 60 | await auth_mgr.refresh_tokens() 61 | except HTTPStatusError as e: 62 | print( 63 | f""" 64 | Could not refresh tokens from {tokens_file}, err={e}\n 65 | You might have to delete the tokens file and re-authenticate 66 | if refresh token is expired 67 | """ 68 | ) 69 | sys.exit(-1) 70 | 71 | # Save the refreshed/updated tokens 72 | with open(tokens_file, mode="w") as f: 73 | f.write(auth_mgr.oauth.json()) 74 | print(f"Refreshed tokens in {tokens_file}!") 75 | 76 | """ 77 | Construct the Xbox API client from AuthenticationManager instance 78 | """ 79 | xbl_client = XboxLiveClient(auth_mgr) 80 | 81 | """ 82 | Some example API calls 83 | """ 84 | # Get friendslist 85 | friendslist = await xbl_client.people.get_friends_own() 86 | print(f"Your friends: {friendslist}\n") 87 | 88 | # Get presence status (by list of XUID) 89 | presence = await xbl_client.presence.get_presence_batch( 90 | ["2533274794093122", "2533274807551369"] 91 | ) 92 | print(f"Statuses of some random players by XUID: {presence}\n") 93 | 94 | # Get messages 95 | messages = await xbl_client.message.get_inbox() 96 | print(f"Your messages: {messages}\n") 97 | 98 | # Get profile by GT 99 | profile = await xbl_client.profile.get_profile_by_gamertag("SomeGamertag") 100 | print(f"Profile under SomeGamertag gamer tag: {profile}\n") 101 | 102 | 103 | asyncio.run(async_main()) 104 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | 10 | [bumpversion:file:xbox/webapi/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bumpversion:file:docs/conf.py] 15 | search = release = '{current_version}' 16 | replace = release = '{new_version}' 17 | 18 | [bdist_wheel] 19 | universal = 1 20 | 21 | [isort] 22 | profile = black 23 | force_sort_within_sections = true 24 | known_first_party = xbox,tests 25 | forced_separate = tests 26 | combine_as_imports = true 27 | 28 | [aliases] 29 | test = pytest 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for XBox Web API.""" 2 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | TEST_PATH = os.path.dirname(__file__) 5 | 6 | 7 | def get_response(name): 8 | """Read a response file.""" 9 | with open(f"{TEST_PATH}/data/responses/{name}.json", encoding="utf8") as f: 10 | return f.read() 11 | 12 | 13 | def get_response_json(name): 14 | """Read a response file and return json dict.""" 15 | text = get_response(name) 16 | return json.loads(text) 17 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | import uuid 3 | 4 | from ecdsa.keys import SigningKey, VerifyingKey 5 | import pytest 6 | import pytest_asyncio 7 | 8 | from xbox.webapi.api.client import XboxLiveClient 9 | from xbox.webapi.authentication.manager import AuthenticationManager 10 | from xbox.webapi.authentication.models import ( 11 | OAuth2TokenResponse, 12 | XAUResponse, 13 | XSTSResponse, 14 | ) 15 | from xbox.webapi.authentication.xal import ( 16 | APP_PARAMS_GAMEPASS_BETA, 17 | CLIENT_PARAMS_ANDROID, 18 | XALManager, 19 | ) 20 | from xbox.webapi.common.request_signer import RequestSigner 21 | from xbox.webapi.common.signed_session import SignedSession 22 | 23 | from tests.common import get_response 24 | 25 | 26 | @pytest_asyncio.fixture(scope="function") 27 | async def auth_mgr(): 28 | session = SignedSession() 29 | mgr = AuthenticationManager(session, "abc", "123", "http://localhost") 30 | mgr.oauth = OAuth2TokenResponse.model_validate_json( 31 | get_response("auth_oauth2_token") 32 | ) 33 | mgr.user_token = XAUResponse.model_validate_json(get_response("auth_user_token")) 34 | mgr.xsts_token = XSTSResponse.model_validate_json(get_response("auth_xsts_token")) 35 | yield mgr 36 | await session.aclose() 37 | 38 | 39 | @pytest_asyncio.fixture(scope="function") 40 | async def xal_mgr(): 41 | session = SignedSession() 42 | mgr = XALManager( 43 | session, 44 | device_id=uuid.UUID("9c493431-5462-4a4a-a247-f6420396318d"), 45 | app_params=APP_PARAMS_GAMEPASS_BETA, 46 | client_params=CLIENT_PARAMS_ANDROID, 47 | ) 48 | yield mgr 49 | await session.aclose() 50 | 51 | 52 | @pytest.fixture(scope="function") 53 | def xbl_client(auth_mgr): 54 | return XboxLiveClient(auth_mgr) 55 | 56 | 57 | @pytest.fixture(scope="session") 58 | def ecdsa_signing_key_str() -> str: 59 | with open("tests/data/test_signing_key.pem") as f: 60 | return f.read() 61 | 62 | 63 | @pytest.fixture(scope="session") 64 | def ecdsa_signing_key(ecdsa_signing_key_str: str) -> SigningKey: 65 | return SigningKey.from_pem(ecdsa_signing_key_str) 66 | 67 | 68 | @pytest.fixture(scope="session") 69 | def ecdsa_verifying_key(ecdsa_signing_key: SigningKey) -> VerifyingKey: 70 | return ecdsa_signing_key.get_verifying_key() 71 | 72 | 73 | @pytest.fixture(scope="session") 74 | def synthetic_request_signer(ecdsa_signing_key) -> RequestSigner: 75 | return RequestSigner(ecdsa_signing_key) 76 | 77 | 78 | @pytest.fixture(scope="session") 79 | def synthetic_timestamp() -> datetime: 80 | return datetime.fromtimestamp(1586999965, timezone.utc) 81 | -------------------------------------------------------------------------------- /tests/data/responses/achievements_360_earned.json: -------------------------------------------------------------------------------- 1 | { 2 | "achievements": [ 3 | { 4 | "id": 6, 5 | "titleId": 1297290392, 6 | "name": "Die fantastischen Fünf", 7 | "sequence": 0, 8 | "flags": 3342345, 9 | "unlockedOnline": true, 10 | "unlocked": true, 11 | "isSecret": false, 12 | "platform": 15, 13 | "gamerscore": 10, 14 | "imageId": 12, 15 | "description": "Ein Farblöscher-Hexa aus 5 Hexas erzeugt", 16 | "lockedDescription": "Erzeuge ein Farblöscher-Hexa aus 5 Hexas", 17 | "type": 1, 18 | "isRevoked": false, 19 | "timeUnlocked": "2014-04-12T14:05:02.0000000Z" 20 | } 21 | ], 22 | "pagingInfo": { 23 | "continuationToken": null, 24 | "totalRecords": 1 25 | }, 26 | "version": "0001-01-01T00:00:00.0000000Z" 27 | } -------------------------------------------------------------------------------- /tests/data/responses/achievements_one_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "achievements": [ 3 | { 4 | "id": "39", 5 | "serviceConfigId": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 6 | "name": "On My Mark", 7 | "titleAssociations": [ 8 | { 9 | "name": "Halo 5: Guardians", 10 | "id": 219630713 11 | } 12 | ], 13 | "progressState": "NotStarted", 14 | "progression": { 15 | "requirements": [ 16 | { 17 | "id": "77f38077-80fc-4d29-9264-e712f311815d", 18 | "current": null, 19 | "target": "1", 20 | "operationType": "Sum", 21 | "valueType": "Integer", 22 | "ruleParticipationType": "Individual" 23 | } 24 | ], 25 | "timeUnlocked": "0001-01-01T00:00:00.0000000Z" 26 | }, 27 | "mediaAssets": [ 28 | { 29 | "name": "e7c471bc-048e-4db5-b2bb-b8ba529eb2ca.png", 30 | "type": "Icon", 31 | "url": "http://images-eds.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToArPGBBXsy0BgxeTKyXK32EuYv17xALJlk1K1zfmFqaHkmnN6MEpNtqpttKYUBs4n3oxhuGpOVAJjax5sEwSy6UqVUeKyiKOds2fCRxelkRTkQ7Fw2sPeo_ibYF4pKXfUowTVrCw.usmZMVtdJVXbgZ" 32 | } 33 | ], 34 | "platforms": [ 35 | "Durango" 36 | ], 37 | "isSecret": false, 38 | "description": "Simultaneously assassinated two Elites in Blue Team co-op.", 39 | "lockedDescription": "Simultaneously assassinate two Elites in Blue Team co-op.", 40 | "productId": "9a924f64-6ac5-4380-b249-8162269c15cc", 41 | "achievementType": "Persistent", 42 | "participationType": "Individual", 43 | "timeWindow": null, 44 | "rewards": [ 45 | { 46 | "name": null, 47 | "description": null, 48 | "value": "10", 49 | "type": "Gamerscore", 50 | "mediaAsset": null, 51 | "valueType": "Int" 52 | } 53 | ], 54 | "estimatedTime": "00:00:00", 55 | "deeplink": null, 56 | "isRevoked": false 57 | } 58 | ], 59 | "pagingInfo": { 60 | "continuationToken": null, 61 | "totalRecords": 1 62 | } 63 | } -------------------------------------------------------------------------------- /tests/data/responses/auth_device_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "IssueInstant": "2010-10-10T03:04:29.6037497Z", 3 | "NotAfter": "2999-10-24T03:04:29.6037497Z", 4 | "Token": "eyJhYoToken", 5 | "DisplayClaims": { 6 | "xdi": { 7 | "did": "F9E13DC7B0997D0D", 8 | "dcs": "0" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/data/responses/auth_oauth2_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "token_type": "bearer", 3 | "expires_in": 3600, 4 | "scope": "Xboxlive.signin Xboxlive.offline_access", 5 | "access_token": "abcdefg", 6 | "refresh_token": "hijklmnop", 7 | "user_id": "123456789" 8 | } 9 | -------------------------------------------------------------------------------- /tests/data/responses/auth_user_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "IssueInstant": "2010-10-10T03:04:29.6037497Z", 3 | "NotAfter": "2999-10-24T03:04:29.6037497Z", 4 | "Token": "abcdefg", 5 | "DisplayClaims": { 6 | "xui": [ 7 | { 8 | "uhs": "abcdefg" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/responses/auth_xsts_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "IssueInstant": "2010-10-10T03:06:35.5251155Z", 3 | "NotAfter": "2999-10-10T19:06:35.5251155Z", 4 | "Token": "123456789", 5 | "DisplayClaims": { 6 | "xui": [ 7 | { 8 | "gtg": "e", 9 | "xid": "2669321029139235", 10 | "uhs": "abcdefg", 11 | "agg": "Adult", 12 | "usr": "", 13 | "utr": "", 14 | "prv": "" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/data/responses/list_add_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "ListTitle": "Pins", 3 | "ListVersion": 1413, 4 | "ListCount": 8, 5 | "AllowDuplicates": false, 6 | "MaxListSize": 200, 7 | "AccessSetting": "OwnerOnly" 8 | } 9 | -------------------------------------------------------------------------------- /tests/data/responses/list_delete_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "ListTitle": "Pins", 3 | "ListVersion": 1414, 4 | "ListCount": 7, 5 | "AllowDuplicates": false, 6 | "MaxListSize": 200, 7 | "AccessSetting": "OwnerOnly" 8 | } -------------------------------------------------------------------------------- /tests/data/responses/lists_get_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImpressionId": "Lists.aa47a315-8a82-4739-a627-805231821ee3", 3 | "ListItems": [ 4 | { 5 | "DateAdded": "08/04/2015 21:06:06", 6 | "DateModified": "08/04/2015 21:06:06", 7 | "Index": 3, 8 | "KValue": 3, 9 | "Item": { 10 | "ItemId": "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe", 11 | "ContentType": "DApp", 12 | "Title": "TV", 13 | "DeviceType": "XboxOne", 14 | "Provider": null, 15 | "ProviderId": null 16 | } 17 | }, 18 | { 19 | "DateAdded": "09/16/2016 19:15:58", 20 | "DateModified": "09/16/2016 19:15:58", 21 | "Index": 4, 22 | "KValue": 4, 23 | "Item": { 24 | "ItemId": "Microsoft.ZuneVideo_8wekyb3d8bbwe", 25 | "ContentType": "DApp", 26 | "Title": "Xbox Video", 27 | "DeviceType": "XboxOne", 28 | "Provider": null, 29 | "ProviderId": null 30 | } 31 | }, 32 | { 33 | "DateAdded": "09/16/2016 19:15:57", 34 | "DateModified": "09/16/2016 19:15:57", 35 | "Index": 5, 36 | "KValue": 5, 37 | "Item": { 38 | "ItemId": "Microsoft.ZuneMusic_8wekyb3d8bbwe", 39 | "ContentType": "DApp", 40 | "Title": "Xbox Music", 41 | "DeviceType": "XboxOne", 42 | "Provider": null, 43 | "ProviderId": null 44 | } 45 | } 46 | ], 47 | "ListMetadata": { 48 | "ListTitle": "Recommended pps", 49 | "ListVersion": 42, 50 | "ListCount": 3, 51 | "AllowDuplicates": false, 52 | "MaxListSize": 200, 53 | "AccessSetting": "OwnerOnly" 54 | } 55 | } -------------------------------------------------------------------------------- /tests/data/responses/mediahub_gameclips_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | { 4 | "commentCount": 0, 5 | "contentId": "62f47208-b873-479b-a9ee-6b16126c43b20", 6 | "contentLocators": [ 7 | { 8 | "expiration": "2022-11-11T11:05:59.3087637Z", 9 | "fileSize": 18825622, 10 | "locatorType": "Download", 11 | "uri": "https://gameclipscontent-d3023.media.xboxlive.com/xuid-2669321029139235-private/62f47208-b873-479b-a9ee-6b16126c43b20.MP4?sv=2015-12-11&sr=b&si=DefaultAccess&sig=OQMDiPhvEw0%2BpitWhQLlpEME%2B1A4hW1OkDI4SZpKvpM%3D&__gda__=1668164759_b54098d230229feb0eb2e06890d2c9ea" 12 | }, 13 | { 14 | "locatorType": "Thumbnail_Small", 15 | "uri": "https://gameclipscontent-t3023.media.xboxlive.com/xuid-2669321029139235-public/62f47208-b873-479b-a9ee-6b16126c43b20_Thumbnail.PNG" 16 | }, 17 | { 18 | "locatorType": "Thumbnail_Large", 19 | "uri": "https://gameclipscontent-t3023.media.xboxlive.com/xuid-2669321029139235-public/62f47208-b873-479b-a9ee-6b16126c43b20_Thumbnail.PNG" 20 | } 21 | ], 22 | "contentSegments": [ 23 | { 24 | "creationType": "UserGenerated", 25 | "creatorChannelId": null, 26 | "creatorXuid": 2669321029139235, 27 | "durationInSeconds": 27, 28 | "offset": 0, 29 | "recordDate": "2018-04-02T19:28:38Z", 30 | "secondaryTitleId": null, 31 | "segmentId": 1, 32 | "titleId": 1717113201 33 | } 34 | ], 35 | "contentState": "Published", 36 | "creationType": "UserGenerated", 37 | "durationInSeconds": 27, 38 | "enforcementState": "None", 39 | "frameRate": 30, 40 | "greatestMomentId": "", 41 | "likeCount": 0, 42 | "localId": "67fac079-f793-4ea8-a448-bd7096a9fac30", 43 | "ownerXuid": 2669321029139235, 44 | "resolutionHeight": 720, 45 | "resolutionWidth": 1280, 46 | "safetyThreshold": "None", 47 | "sandboxId": "RETAIL", 48 | "sessions": [], 49 | "shareCount": 0, 50 | "sharedTo": [], 51 | "titleData": "", 52 | "titleId": 1717113201, 53 | "titleName": "Sea of Thieves", 54 | "tournaments": [], 55 | "uploadDate": "2018-04-02T19:46:26.1665177Z", 56 | "uploadDeviceType": "Edmonton", 57 | "uploadLanguage": "de-DE", 58 | "uploadRegion": "DE", 59 | "uploadTitleId": 49312658, 60 | "userCaption": "", 61 | "viewCount": 1 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /tests/data/responses/mediahub_screenshots_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | { 4 | "captureDate": "2022-01-08T19:49:17Z", 5 | "contentId": "0b2a0559-a246-911a-9a06-e01f3d38ec5b", 6 | "contentLocators": [ 7 | { 8 | "fileSize": 17030676, 9 | "locatorType": "Download", 10 | "uri": "https://screenshotscontent-d4002.media.xboxlive.com/xuid-2669321029139235-private/0b2a0559-a246-911a-9a06-e01f3d38ec5b.PNG?sv=2015-12-11&sr=b&si=DefaultAccess" 11 | }, 12 | { 13 | "locatorType": "Thumbnail_Small", 14 | "uri": "https://screenshotscontent-t4002.media.xboxlive.com/xuid-2669321029139235-public/0b2a0559-a246-911a-9a06-e01f3d38ec5b_Thumbnail.PNG" 15 | }, 16 | { 17 | "locatorType": "Thumbnail_Large", 18 | "uri": "https://screenshotscontent-t4002.media.xboxlive.com/xuid-2669321029139235-public/0b2a0559-a246-911a-9a06-e01f3d38ec5b_Thumbnail.PNG" 19 | }, 20 | { 21 | "fileSize": 14637915, 22 | "locatorType": "Download_HDR", 23 | "uri": "https://screenshotscontent-d4002.media.xboxlive.com/xuid-2669321029139235-private/0b2a0559-a246-911a-9a06-e01f3d38ec5b.JXR?sv=2015-12-11&sr=b&si=DefaultAccess" 24 | } 25 | ], 26 | "CreationType": "UserGenerated", 27 | "localId": "62f47208-b873-479b-a9ee-6b16126c43b20", 28 | "ownerXuid": 2669321029139235, 29 | "resolutionHeight": 2160, 30 | "resolutionWidth": 3840, 31 | "sandboxId": "RETAIL", 32 | "sharedTo": [], 33 | "titleId": 1777860928, 34 | "titleName": "Microsoft Flight Simulator", 35 | "dateUploaded": "2022-01-08T19:54:14.6516641Z", 36 | "uploadLanguage": "de-DE", 37 | "uploadRegion": "DE", 38 | "uploadTitleId": 49312658, 39 | "uploadDeviceType": "Scarlett", 40 | "commentCount": 0, 41 | "likeCount": 0, 42 | "shareCount": 0, 43 | "viewCount": 0, 44 | "contentState": "Published", 45 | "enforcementState": "None", 46 | "safetyThreshold": "Unscanned", 47 | "sessions": [], 48 | "tournaments": [] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /tests/data/responses/message_get_conversation.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2020-10-09T02:05:48.9846784Z", 3 | "networkId": "Xbox", 4 | "type": "OneToOne", 5 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 6 | "participants": [ 7 | "2533274883751843", 8 | "2719584417856940" 9 | ], 10 | "readHorizon": "14670727301543890", 11 | "deleteHorizon": "14670705998559210", 12 | "isRead": true, 13 | "muted": false, 14 | "folder": "Primary", 15 | "messages": [ 16 | { 17 | "contentPayload": { 18 | "content": { 19 | "parts": [ 20 | { 21 | "contentType": "text", 22 | "text": "Deleted", 23 | "version": 0 24 | } 25 | ] 26 | } 27 | }, 28 | "timestamp": "2020-10-09T02:05:44.5938743Z", 29 | "lastUpdateTimestamp": "2020-10-09T02:06:00.7038298Z", 30 | "type": "ContentMessage", 31 | "networkId": "Xbox", 32 | "conversationType": "OneToOne", 33 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 34 | "sender": "2719584417856940", 35 | "messageId": "14670726562813906", 36 | "isDeleted": true, 37 | "isServerUpdated": false 38 | }, 39 | { 40 | "contentPayload": { 41 | "content": { 42 | "parts": [ 43 | { 44 | "contentType": "text", 45 | "version": 0, 46 | "text": "Test 4", 47 | "unsuitableFor": [] 48 | } 49 | ] 50 | } 51 | }, 52 | "timestamp": "2020-10-09T02:05:48.9846784Z", 53 | "lastUpdateTimestamp": "2020-10-09T02:05:48.9846784Z", 54 | "type": "ContentMessage", 55 | "networkId": "Xbox", 56 | "conversationType": "OneToOne", 57 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 58 | "sender": "2719584417856940", 59 | "messageId": "14670727301543890", 60 | "isDeleted": false, 61 | "isServerUpdated": false 62 | } 63 | ], 64 | "continuationToken": null, 65 | "voiceId": "05907fa3-0000-0009-acbd-299772a90900", 66 | "voiceRoster": [] 67 | } -------------------------------------------------------------------------------- /tests/data/responses/message_get_inbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "primary": { 3 | "folder": "Primary", 4 | "totalCount": 24, 5 | "unreadCount": 1, 6 | "conversations": [ 7 | { 8 | "timestamp": "2020-10-09T01:35:32.2655859Z", 9 | "networkId": "Xbox", 10 | "type": "OneToOne", 11 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 12 | "voiceId": "05907fa3-0000-0009-acbd-299772a90900", 13 | "participants": ["2533274883751843", "2719584417856940"], 14 | "readHorizon": "14670422513694688", 15 | "deleteHorizon": "0", 16 | "isRead": true, 17 | "muted": false, 18 | "folder": "Primary", 19 | "lastMessage": { 20 | "contentPayload": { 21 | "content": { 22 | "parts": [ 23 | { 24 | "contentType": "text", 25 | "version": 0, 26 | "text": "Test", 27 | "unsuitableFor": [] 28 | } 29 | ] 30 | } 31 | }, 32 | "timestamp": "2020-10-09T01:35:32.2655859Z", 33 | "lastUpdateTimestamp": "2020-10-09T01:35:32.2655859Z", 34 | "type": "ContentMessage", 35 | "networkId": "Xbox", 36 | "conversationType": "OneToOne", 37 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 38 | "owner": 2719584417856940, 39 | "sender": "2719584417856940", 40 | "messageId": "14670422513694688", 41 | "isDeleted": false, 42 | "isServerUpdated": false 43 | } 44 | }, 45 | { 46 | "timestamp": "2020-10-09T00:35:57.8664824Z", 47 | "networkId": "Xbox", 48 | "type": "OneToOne", 49 | "conversationId": "00000000-0000-0000-acbd-299772a90900", 50 | "voiceId": "00000000-0000-0000-acbd-299772a90900", 51 | "participants": ["0", "2719584417856940"], 52 | "readHorizon": "14449092124444632", 53 | "deleteHorizon": "6930255230021639", 54 | "isRead": false, 55 | "muted": false, 56 | "folder": "Primary", 57 | "lastMessage": { 58 | "contentPayload": { 59 | "content": { 60 | "parts": [ 61 | { 62 | "contentType": "text", 63 | "text": "Great news: Game Pass is about to get even better! Starting November 10th 2020, EA Play, the gaming membership from Electronic Arts, will be included with your Xbox Game Pass Ultimate membership at no additional cost. See xbox.com/gamepass for details.", 64 | "version": 0 65 | } 66 | ] 67 | } 68 | }, 69 | "timestamp": "2020-10-09T00:35:57.8664824Z", 70 | "lastUpdateTimestamp": "2020-10-09T00:35:57.8664824Z", 71 | "type": "ContentMessage", 72 | "networkId": "Xbox", 73 | "conversationType": "OneToOne", 74 | "conversationId": "00000000-0000-0000-acbd-299772a90900", 75 | "owner": 2719584417856940, 76 | "sender": "0", 77 | "messageId": "14669822834366440", 78 | "isDeleted": false, 79 | "isServerUpdated": false 80 | } 81 | } 82 | ] 83 | }, 84 | "folders": [], 85 | "safetySettings": { 86 | "version": 3, 87 | "primaryInboxMedia": "Mature", 88 | "primaryInboxText": "AllowAll", 89 | "primaryInboxUrl": "Mature", 90 | "secondaryInboxMedia": "Medium", 91 | "secondaryInboxText": "Medium", 92 | "secondaryInboxUrl": "BlockAll", 93 | "canUnobscure": true 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/data/responses/message_new_conversation.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "0001-01-01T00:00:00", 3 | "networkId": "Xbox", 4 | "type": "OneToOne", 5 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900", 6 | "participants": null, 7 | "readHorizon": "0", 8 | "deleteHorizon": "0", 9 | "isRead": false, 10 | "muted": false, 11 | "folder": "Unspecified", 12 | "messages": null, 13 | "continuationToken": null, 14 | "voiceId": "05907fa3-0000-0009-acbd-299772a90900", 15 | "voiceRoster": null 16 | } -------------------------------------------------------------------------------- /tests/data/responses/message_send_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "messageId": "14670963043022811", 3 | "conversationId": "05907fa3-0000-0009-acbd-299772a90900" 4 | } -------------------------------------------------------------------------------- /tests/data/responses/people_summary_by_gamertag.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetFollowingCount": 0, 3 | "targetFollowerCount": 27660, 4 | "isCallerFollowingTarget": false, 5 | "isTargetFollowingCaller": false, 6 | "hasCallerMarkedTargetAsFavorite": false, 7 | "hasCallerMarkedTargetAsIdentityShared": false, 8 | "legacyFriendStatus": "None" 9 | } -------------------------------------------------------------------------------- /tests/data/responses/people_summary_by_xuid.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetFollowingCount": 0, 3 | "targetFollowerCount": 27660, 4 | "isCallerFollowingTarget": false, 5 | "isTargetFollowingCaller": false, 6 | "hasCallerMarkedTargetAsFavorite": false, 7 | "hasCallerMarkedTargetAsIdentityShared": false, 8 | "legacyFriendStatus": "None" 9 | } -------------------------------------------------------------------------------- /tests/data/responses/people_summary_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetFollowingCount": 121, 3 | "targetFollowerCount": 105, 4 | "isCallerFollowingTarget": false, 5 | "isTargetFollowingCaller": false, 6 | "hasCallerMarkedTargetAsFavorite": false, 7 | "hasCallerMarkedTargetAsIdentityShared": false, 8 | "legacyFriendStatus": "None", 9 | "availablePeopleSlots": 928, 10 | "recentChangeCount": 3, 11 | "watermark": "5249064048202941621" 12 | } -------------------------------------------------------------------------------- /tests/data/responses/presence.json: -------------------------------------------------------------------------------- 1 | { 2 | "xuid": "2669321029139235", 3 | "state": "Offline", 4 | "lastSeen": { 5 | "deviceType": "iOS", 6 | "titleId": "1016898439", 7 | "titleName": "", 8 | "timestamp": "2021-06-11T22:59:53.0944615Z" 9 | } 10 | } -------------------------------------------------------------------------------- /tests/data/responses/presence_batch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "xuid": "2669321029139235", 4 | "state": "Offline", 5 | "lastSeen": { 6 | "deviceType": "Android", 7 | "titleId": "328178078", 8 | "titleName": "", 9 | "timestamp": "2018-03-28T00:26:51.392459Z" 10 | } 11 | }, 12 | { 13 | "xuid": "2584878536129841", 14 | "state": "Offline", 15 | "lastSeen": { 16 | "deviceType": "XboxOne", 17 | "titleId": "750323071", 18 | "titleName": "", 19 | "timestamp": "2018-03-27T02:19:03.1204387Z" 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /tests/data/responses/presence_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "xuid": "2535428504476914", 3 | "state": "Offline" 4 | } -------------------------------------------------------------------------------- /tests/data/responses/profile_by_gamertag.json: -------------------------------------------------------------------------------- 1 | { 2 | "profileUsers": [ 3 | { 4 | "id": "2669321029139235", 5 | "hostId": "2669321029139235", 6 | "settings": [ 7 | { 8 | "id": "Gamertag", 9 | "value": "e" 10 | }, 11 | { 12 | "id": "ModernGamertag", 13 | "value": "e" 14 | }, 15 | { 16 | "id": "ModernGamertagSuffix", 17 | "value": "" 18 | }, 19 | { 20 | "id": "UniqueModernGamertag", 21 | "value": "e" 22 | }, 23 | { 24 | "id": "RealNameOverride", 25 | "value": "Eric Neustadter" 26 | }, 27 | { 28 | "id": "Bio", 29 | "value": "ex-Xbox" 30 | }, 31 | { 32 | "id": "Location", 33 | "value": "about.me/thevowel" 34 | }, 35 | { 36 | "id": "Gamerscore", 37 | "value": "98096" 38 | }, 39 | { 40 | "id": "GameDisplayPicRaw", 41 | "value": "https://images-eds-ssl.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIl_5PaIHeJ592LG1L.uCOWneEDBhHctvuC06CzW38LKNYiVGTXqxcsQqYGIrr8hFz&format=png" 42 | }, 43 | { 44 | "id": "TenureLevel", 45 | "value": "18" 46 | }, 47 | { 48 | "id": "AccountTier", 49 | "value": "Gold" 50 | }, 51 | { 52 | "id": "XboxOneRep", 53 | "value": "Superstar" 54 | }, 55 | { 56 | "id": "PreferredColor", 57 | "value": "http://dlassets.xboxlive.com/public/content/ppl/colors/00000.json" 58 | }, 59 | { 60 | "id": "Watermarks", 61 | "value": "XboxOriginalTeam|XboxLiveLaunchTeam|LaunchTeam|NxeTeam|KinectTeam|XboxOneTeam|XboxNxoeTeam" 62 | }, 63 | { 64 | "id": "IsQuarantined", 65 | "value": "0" 66 | } 67 | ], 68 | "isSponsoredUser": false 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /tests/data/responses/profile_by_xuid.json: -------------------------------------------------------------------------------- 1 | { 2 | "profileUsers": [ 3 | { 4 | "id": "2669321029139235", 5 | "hostId": "2669321029139235", 6 | "settings": [ 7 | { 8 | "id": "Gamertag", 9 | "value": "e" 10 | }, 11 | { 12 | "id": "ModernGamertag", 13 | "value": "e" 14 | }, 15 | { 16 | "id": "ModernGamertagSuffix", 17 | "value": "" 18 | }, 19 | { 20 | "id": "UniqueModernGamertag", 21 | "value": "e" 22 | }, 23 | { 24 | "id": "RealNameOverride", 25 | "value": "Eric Neustadter" 26 | }, 27 | { 28 | "id": "Bio", 29 | "value": "ex-Xbox" 30 | }, 31 | { 32 | "id": "Location", 33 | "value": "about.me/thevowel" 34 | }, 35 | { 36 | "id": "Gamerscore", 37 | "value": "98096" 38 | }, 39 | { 40 | "id": "GameDisplayPicRaw", 41 | "value": "https://images-eds-ssl.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIl_5PaIHeJ592LG1L.uCOWneEDBhHctvuC06CzW38LKNYiVGTXqxcsQqYGIrr8hFz&format=png" 42 | }, 43 | { 44 | "id": "TenureLevel", 45 | "value": "18" 46 | }, 47 | { 48 | "id": "AccountTier", 49 | "value": "Gold" 50 | }, 51 | { 52 | "id": "XboxOneRep", 53 | "value": "Superstar" 54 | }, 55 | { 56 | "id": "PreferredColor", 57 | "value": "http://dlassets.xboxlive.com/public/content/ppl/colors/00000.json" 58 | }, 59 | { 60 | "id": "Watermarks", 61 | "value": "XboxOriginalTeam|XboxLiveLaunchTeam|LaunchTeam|NxeTeam|KinectTeam|XboxOneTeam|XboxNxoeTeam" 62 | }, 63 | { 64 | "id": "IsQuarantined", 65 | "value": "0" 66 | } 67 | ], 68 | "isSponsoredUser": false 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_recent_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_recent_own_titleid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_recent_xuid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_recent_xuid_titleid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_saved_own.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_saved_own_titleid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_saved_xuid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/screenshots_saved_xuid_titleid.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshots": [ 3 | { 4 | "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", 5 | "resolutionHeight": 1080, 6 | "resolutionWidth": 1920, 7 | "state": "Published", 8 | "datePublished": "2015-11-11T06:11:58.6404578Z", 9 | "dateTaken": "2015-11-11T06:10:16Z", 10 | "lastModified": "2015-11-11T06:10:16Z", 11 | "userCaption": "", 12 | "type": "UserGenerated", 13 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 14 | "titleId": 219630713, 15 | "rating": 0.0, 16 | "ratingCount": 0, 17 | "views": 413, 18 | "titleData": "", 19 | "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", 20 | "savedByUser": true, 21 | "achievementId": "", 22 | "greatestMomentId": null, 23 | "thumbnails": [ 24 | { 25 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", 26 | "fileSize": 95044, 27 | "thumbnailType": "Small" 28 | }, 29 | { 30 | "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", 31 | "fileSize": 340052, 32 | "thumbnailType": "Large" 33 | } 34 | ], 35 | "screenshotUris": [ 36 | { 37 | "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", 38 | "fileSize": 1790858, 39 | "uriType": "Download", 40 | "expiration": "2018-03-29T17:46:58.5342894Z" 41 | } 42 | ], 43 | "xuid": "2669321029139235", 44 | "screenshotName": "", 45 | "titleName": "Halo 5: Guardians", 46 | "screenshotLocale": "en-US", 47 | "screenshotContentAttributes": "None", 48 | "deviceType": "XboxOne" 49 | } 50 | ], 51 | "pagingInfo": { 52 | "continuationToken": null 53 | } 54 | } -------------------------------------------------------------------------------- /tests/data/responses/smartglass_command.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "result": null, 7 | "uiText": null, 8 | "destination": { 9 | "id": "ABCDEFG", 10 | "name": "XONEX", 11 | "locale": "en-US", 12 | "powerState": "ConnectedStandby", 13 | "remoteManagementEnabled": "True", 14 | "consoleStreamingEnabled": "False", 15 | "consoleType": "XboxOneX", 16 | "wirelessWarning": "", 17 | "outOfHomeWarning": "" 18 | }, 19 | "userInfo": null, 20 | "opId": "35bd7870-fad4-4e98-a354-d027bd840116" 21 | } 22 | -------------------------------------------------------------------------------- /tests/data/responses/smartglass_console_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "result": [ 7 | { 8 | "id": "ABCDEFG", 9 | "name": "XONEX", 10 | "locale": "en-US", 11 | "consoleType": "XboxOneX", 12 | "powerState": "ConnectedStandby", 13 | "digitalAssistantRemoteControlEnabled": true, 14 | "remoteManagementEnabled": true, 15 | "consoleStreamingEnabled": false, 16 | "storageDevices": [ 17 | { 18 | "storageDeviceId": "1", 19 | "storageDeviceName": "Internal", 20 | "isDefault": true, 21 | "freeSpaceBytes": 236267835392.0, 22 | "totalSpaceBytes": 838592360448.0 23 | } 24 | ] 25 | }, 26 | { 27 | "id": "HIJKLMN", 28 | "name": "XONE", 29 | "locale": "en-US", 30 | "consoleType": "XboxOne", 31 | "powerState": "ConnectedStandby", 32 | "digitalAssistantRemoteControlEnabled": true, 33 | "remoteManagementEnabled": true, 34 | "consoleStreamingEnabled": false, 35 | "storageDevices": [ 36 | { 37 | "storageDeviceId": "2", 38 | "storageDeviceName": "Internal", 39 | "isDefault": false, 40 | "freeSpaceBytes": 147163541504.0, 41 | "totalSpaceBytes": 391915761664.0 42 | }, 43 | { 44 | "storageDeviceId": "3", 45 | "storageDeviceName": "External", 46 | "isDefault": true, 47 | "freeSpaceBytes": 3200714067968.0, 48 | "totalSpaceBytes": 4000787029504.0 49 | } 50 | ] 51 | } 52 | ], 53 | "agentUserId": null 54 | } 55 | -------------------------------------------------------------------------------- /tests/data/responses/smartglass_console_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "powerState": "ConnectedStandby", 7 | "playbackState": "Stopped", 8 | "loginState": null, 9 | "focusAppAumid": "4DF9E0F8.Netflix_mcm4njqhnhss8!App", 10 | "isTvConfigured": true, 11 | "digitalAssistantRemoteControlEnabled": true, 12 | "consoleStreamingEnabled": false, 13 | "remoteManagementEnabled": true 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/responses/smartglass_installed_apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "result": [ 7 | { 8 | "oneStoreProductId": "C2DCVRMJ9V45", 9 | "titleId": 12206284, 10 | "aumid": "00BA40CC-US_fc7h7ec1dwtc0!App", 11 | "lastActiveTime": "2020-10-07T21:00:48.036Z", 12 | "isGame": true, 13 | "name": "eFootball PES 2020", 14 | "contentType": "Game", 15 | "instanceId": "{A89ECE52-7E8E-444F-BBD0-C68B76C2ECA4}#AC093B66-C37C-4F95-BD9B-1817C789E41F", 16 | "storageDeviceId": "A89ECE52-7E8E-444F-BBD0-C68B76C2ECA4", 17 | "uniqueId": "AC093B66-C37C-4F95-BD9B-1817C789E41F", 18 | "legacyProductId": "DF1B4514-2E4F-47B5-B3DD-F10B0CC6718A", 19 | "version": 281509336514560.0, 20 | "sizeInBytes": 43352756224.0, 21 | "installTime": "2019-12-15T22:51:36.464Z", 22 | "updateTime": "2020-07-18T22:35:46.912Z", 23 | "parentId": null 24 | }, 25 | { 26 | "oneStoreProductId": "9WZDNCRFJ3TJ", 27 | "titleId": 327370029, 28 | "aumid": "4DF9E0F8.Netflix_mcm4njqhnhss8!App", 29 | "lastActiveTime": "2020-10-08T01:48:30.203Z", 30 | "isGame": false, 31 | "name": "Netflix", 32 | "contentType": "App", 33 | "instanceId": "{A89ECE52-7E8E-444F-BBD0-C68B76C2ECA4}#4DF9E0F8.Netflix_mcm4njqhnhss8", 34 | "storageDeviceId": "A89ECE52-7E8E-444F-BBD0-C68B76C2ECA4", 35 | "uniqueId": "4DF9E0F8.Netflix_mcm4njqhnhss8", 36 | "legacyProductId": null, 37 | "version": 2251799815651398.0, 38 | "sizeInBytes": 7250253.0, 39 | "installTime": "2017-11-08T04:06:44.746Z", 40 | "updateTime": "2019-02-04T23:42:42.422Z", 41 | "parentId": null 42 | } 43 | ], 44 | "agentUserId": null 45 | } 46 | -------------------------------------------------------------------------------- /tests/data/responses/smartglass_op_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "opStatusList": [ 7 | { 8 | "operationStatus": "Pending", 9 | "opId": "35bd7870-fad4-4e98-a354-d027bd840116", 10 | "originatingSessionId": "91b3af51-b59e-4d4c-b0d7-6acb6e3de586", 11 | "command": "WakeUp", 12 | "succeeded": false, 13 | "consoleStatusCode": null, 14 | "xccsErrorCode": null, 15 | "hResult": null, 16 | "message": null 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/data/responses/smartglass_storage_devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "errorCode": "OK", 4 | "errorMessage": null 5 | }, 6 | "deviceId": "ABCDEFG", 7 | "result": [ 8 | { 9 | "storageDeviceId": "1", 10 | "storageDeviceName": "Internal", 11 | "isDefault": true, 12 | "freeSpaceBytes": 236267835392.0, 13 | "totalSpaceBytes": 838592360448.0 14 | } 15 | ], 16 | "agentUserId": null 17 | } 18 | -------------------------------------------------------------------------------- /tests/data/responses/usersearch_live_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "text": "Tux", 5 | "result": { 6 | "id": "2533274895244106", 7 | "gamertag": "Tux", 8 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RWwcxuUQ9WVT6xh5XaeeZD02wEfGZeuD.XMoGFVYkwHDqfVSYLAdvK.9IlH2sAzAwWZgwaT8xNIzuGLxZUs7WYDCernQAa2Z5pcI7ZPLMjUFqy4Exhco5lsn87_QraMalCIh3jlnlF_sjFT2adTsb1KU-&format=png", 9 | "score": 0.0 10 | } 11 | }, 12 | { 13 | "text": "TuX Rose", 14 | "result": { 15 | "id": "2533274826466726", 16 | "gamertag": "TuX Rose", 17 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW8ke8ralOdP9BGd4wzwl0MJ9z6QzuGwZjtvbE7sSsMVW_zJPeygmghEEz6SlvdwPTkfnI_ifxHEl8Bxf65VhimlzjtjbADgPIeBFg77KAaPgvMR.2gkcQhtuRsRdPDBFfnpNqFQ2zyMr6OPEEWveG_M-&format=png", 18 | "score": 0.0 19 | } 20 | }, 21 | { 22 | "text": "TuX Teal", 23 | "result": { 24 | "id": "2535469793482901", 25 | "gamertag": "TuX Teal", 26 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=8Oaj9Ryq1G1_p3lLnXlsaZgGzAie6Mnu24_PawYuDYIoH77pJ.X5Z.MqQPibUVTcS9jr0n8i7LY1tL3U7AiafRx1EB3W8XvHFH6tP.92dvnlFSq13CMXDFG2UU0OgswX&format=png", 27 | "score": 0.0 28 | } 29 | }, 30 | { 31 | "text": "Gizmo Tux", 32 | "result": { 33 | "id": "2535472607389917", 34 | "gamertag": "Gizmo Tux", 35 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW3apWESZjav65Yncai8aRmVbSlZ3zqRpg1sdxEje_JmFd0H6LIaW2ACNGPTs2dTgjJETJdbc1Ezx_MDjAQjdC86eF6svmSljZHgg8MgDc.IcJgySZUyO9xeVEwTxEXUPRlBNI.FoHt2JSkgWNLa7_CQ-&format=png", 36 | "score": 0.0 37 | } 38 | }, 39 | { 40 | "text": "Tennessee Tux", 41 | "result": { 42 | "id": "2533274889256608", 43 | "gamertag": "Tennessee Tux", 44 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=Hr2eiH8yWKd4q_oa.xgbMn_Nr81xI0exxmgLbuOz_JFlA0OUi9rGwuMNOXkIGIIEbTYKWtpweV9EYJKhI2LFWWLEphXu5ydxODNou70VhWzXrVXGMJs_wC2fcy7VPqXJM.3mjqhvN1NXBBqgmvYE6oOhmCX8hnAubNFdO5HubCQ-&format=png", 45 | "score": 0.0 46 | } 47 | }, 48 | { 49 | "text": "Tux Sharkk", 50 | "result": { 51 | "id": "2535429744373000", 52 | "gamertag": "Tux Sharkk", 53 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIFXxmxGDtE9Vkd62rOpb7JcGvME9LzjeruYo3cC50qVYelz5LjucMJtB5xOqvr7WR&format=png", 54 | "score": 0.0 55 | } 56 | }, 57 | { 58 | "text": "Tux Rabbit", 59 | "result": { 60 | "id": "2535412136281412", 61 | "gamertag": "Tux Rabbit", 62 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIoIeJ1sl4QgwrJRGcdRHOCkEH9VceK7CPhb1R4StLimFchduLrEzQbBvKlLplSZcV&format=png", 63 | "score": 0.0 64 | } 65 | }, 66 | { 67 | "text": "Capitan Tux", 68 | "result": { 69 | "id": "2533274892415915", 70 | "gamertag": "Capitan Tux", 71 | "displayPicUri": "http://images-eds.xboxlive.com/image?url=rwljod2fPqLqGP3DBV9F_yK9iuxAt3_MH6tcOnQXTc8q7ySyo6uynPK38pcHGWS_yc8P7cJ2zzHnGdWznLsSVZOwShXEWkyF0R6U6wXK0rs-&background=0xababab&mode=Padding&format=png", 72 | "score": 0.0 73 | } 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /tests/data/responses/userstats_batch_by_scid.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "name": "Hero", 5 | "statlistscollection": [] 6 | } 7 | ], 8 | "statlistscollection": [ 9 | { 10 | "arrangebyfield": "xuid", 11 | "arrangebyfieldid": "2669321029139235", 12 | "stats": [ 13 | { 14 | "groupproperties": {}, 15 | "xuid": "2669321029139235", 16 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 17 | "name": "MinutesPlayed", 18 | "type": "Integer", 19 | "value": "1220", 20 | "properties": {} 21 | } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /tests/data/responses/userstats_by_scid.json: -------------------------------------------------------------------------------- 1 | { 2 | "statlistscollection": [ 3 | { 4 | "arrangebyfield": "xuid", 5 | "arrangebyfieldid": "2669321029139235", 6 | "stats": [ 7 | { 8 | "xuid": "2669321029139235", 9 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 10 | "name": "MinutesPlayed", 11 | "type": "Integer", 12 | "value": "1220", 13 | "properties": {} 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tests/data/responses/userstats_by_scid_with_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "statlistscollection": [ 3 | { 4 | "arrangebyfield": "xuid", 5 | "arrangebyfieldid": "2669321029139235", 6 | "stats": [ 7 | { 8 | "xuid": "2669321029139235", 9 | "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", 10 | "name": "MinutesPlayed", 11 | "type": "Integer", 12 | "value": "1220", 13 | "properties": {} 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tests/data/responses/xal_authentication_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "MsaOauthRedirect":"https://login.live.com/oauth20_authorize.srf?lw=1&fl=dob,easi2&xsup=1&display=android_phone&code_challenge=code_challenge_string&code_challenge_method=S256&state=state_string&client_id=000000004C20A908&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=ms-xal-public-beta-000000004c20a908%3A%2F%2Fauth&nopa=2", 3 | "MsaRequestParameters":{} 4 | } -------------------------------------------------------------------------------- /tests/data/responses/xal_authorization_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceToken": "eyDeviceToken", 3 | "TitleToken": { 4 | "DisplayClaims": { 5 | "xti": { 6 | "tid": "1016898439" 7 | } 8 | }, 9 | "IssueInstant": "2022-11-11T21:11:43.9456623Z", 10 | "NotAfter": "2099-11-25T21:11:43.9456623Z", 11 | "Token": "eyTitletoken" 12 | }, 13 | "UserToken": { 14 | "DisplayClaims": { 15 | "xui": [ 16 | { 17 | "uhs": "2034583485034500345" 18 | } 19 | ] 20 | }, 21 | "IssueInstant": "2022-11-11T21:11:43.9422756Z", 22 | "NotAfter": "2099-11-25T21:11:43.9422756Z", 23 | "Token": "eyUserToken" 24 | }, 25 | "AuthorizationToken": { 26 | "DisplayClaims": { 27 | "xui": [ 28 | { 29 | "gtg": "Pony", 30 | "xid": "24812480912094", 31 | "uhs": "2034583485034500345", 32 | "agg": "Adult", 33 | "usr": "195 229 243", 34 | "prv": "184 185 186 187 188 190 191 192 193 194 196 198 199 200 201 203 204 205 206 207 208 211 214 215 216 217 220 224 227 228 235 238 245 247 249 252 254 255" 35 | } 36 | ] 37 | }, 38 | "IssueInstant": "2022-11-11T21:11:44.1945885Z", 39 | "NotAfter": "2099-11-12T13:11:44.1945885Z", 40 | "Token": "eyXSTSToken" 41 | }, 42 | "WebPage": "https://sisu.xboxlive.com/client/v27/000000004c20a908/view/index.html", 43 | "Sandbox": "RETAIL" 44 | } -------------------------------------------------------------------------------- /tests/data/test_signing_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIObr5IVtB+DQcn25+R9n4K/EyUUSbVvxIJY7WhVeELUuoAoGCCqGSM49AwEHoUQDQgAE 3 | OKyCQ9qH5U4lZcS0c5/LxIyKvOpKe0l3x4Eg5OgDbzezKNLRgT28fd4Fq3rU/1OQKmx6jSq0vTB5 4 | Ao/48m0iGg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- 1 | from httpx import HTTPStatusError, Response 2 | import pytest 3 | 4 | from xbox.webapi.api.provider.account.models import ( 5 | ChangeGamertagResult, 6 | ClaimGamertagResult, 7 | ) 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_claim_gamertag(respx_mock, xbl_client): 12 | route = respx_mock.post("https://user.mgt.xboxlive.com").mock( 13 | return_value=Response(200) 14 | ) 15 | ret = await xbl_client.account.claim_gamertag("2669321029139235", "PrettyPony") 16 | 17 | assert ret == ClaimGamertagResult.Available 18 | assert route.called 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_claim_gamertag_error(respx_mock, xbl_client): 23 | route = respx_mock.post("https://user.mgt.xboxlive.com").mock( 24 | return_value=Response(500) 25 | ) 26 | with pytest.raises(HTTPStatusError) as err: 27 | await xbl_client.account.claim_gamertag("2669321029139235", "PrettyPony") 28 | 29 | assert err.value.response.status_code == 500 30 | assert route.called 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_change_gamertag(respx_mock, xbl_client): 35 | route = respx_mock.post("https://accounts.xboxlive.com").mock( 36 | return_value=Response(200) 37 | ) 38 | ret = await xbl_client.account.change_gamertag("2669321029139235", "PrettyPony") 39 | 40 | assert ret == ChangeGamertagResult.ChangeSuccessful 41 | assert route.called 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_change_gamertag_error(respx_mock, xbl_client): 46 | route = respx_mock.post("https://accounts.xboxlive.com").mock( 47 | return_value=Response(500) 48 | ) 49 | with pytest.raises(HTTPStatusError) as err: 50 | await xbl_client.account.change_gamertag("2669321029139235", "PrettyPony") 51 | 52 | assert err.value.response.status_code == 500 53 | assert route.called 54 | -------------------------------------------------------------------------------- /tests/test_achievements.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_achievement_360_all(respx_mock, xbl_client): 9 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("achievements_360_all")) 11 | ) 12 | 13 | ret = await xbl_client.achievements.get_achievements_xbox360_all( 14 | "2669321029139235", 1297290392 15 | ) 16 | 17 | assert ret.paging_info.total_records == 15 18 | assert route.called 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_achievement_360_earned(respx_mock, xbl_client): 23 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 24 | return_value=Response(200, json=get_response_json("achievements_360_earned")) 25 | ) 26 | 27 | ret = await xbl_client.achievements.get_achievements_xbox360_earned( 28 | "2669321029139235", 1297290392 29 | ) 30 | 31 | assert len(ret.achievements) == 1 32 | assert route.called 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_achievement_360_recent_progress(respx_mock, xbl_client): 37 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 38 | return_value=Response( 39 | 200, json=get_response_json("achievements_360_recent_progress") 40 | ) 41 | ) 42 | 43 | ret = ( 44 | await xbl_client.achievements.get_achievements_xbox360_recent_progress_and_info( 45 | xuid="2669321029139235" 46 | ) 47 | ) 48 | 49 | assert len(ret.titles) == 32 50 | assert route.called 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_achievement_one_details(respx_mock, xbl_client): 55 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 56 | return_value=Response(200, json=get_response_json("achievements_one_details")) 57 | ) 58 | 59 | ret = await xbl_client.achievements.get_achievements_detail_item( 60 | xuid="2669321029139235", 61 | service_config_id="1370999b-fca2-4c53-8ec5-73493bcb67e5", 62 | achievement_id="39", 63 | ) 64 | 65 | assert len(ret.achievements) == 1 66 | assert route.called 67 | 68 | 69 | @pytest.mark.asyncio 70 | async def test_achievement_one_gameprogress(respx_mock, xbl_client): 71 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 72 | return_value=Response( 73 | 200, json=get_response_json("achievements_one_gameprogress") 74 | ) 75 | ) 76 | 77 | ret = await xbl_client.achievements.get_achievements_xboxone_gameprogress( 78 | xuid="2669321029139235", title_id=219630713 79 | ) 80 | 81 | assert len(ret.achievements) == 32 82 | assert route.called 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_achievement_one_recent_progress(respx_mock, xbl_client): 87 | route = respx_mock.get("https://achievements.xboxlive.com").mock( 88 | return_value=Response( 89 | 200, json=get_response_json("achievements_one_recent_progress") 90 | ) 91 | ) 92 | 93 | ret = ( 94 | await xbl_client.achievements.get_achievements_xboxone_recent_progress_and_info( 95 | xuid="2669321029139235" 96 | ) 97 | ) 98 | 99 | assert len(ret.titles) == 32 100 | assert route.called 101 | -------------------------------------------------------------------------------- /tests/test_catalog.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from xbox.webapi.api.provider.catalog.models import AlternateIdType, FieldsTemplate 5 | 6 | from tests.common import get_response_json 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_get_products(respx_mock, xbl_client): 11 | route = respx_mock.get("https://displaycatalog.mp.microsoft.com").mock( 12 | return_value=Response(200, json=get_response_json("catalog_browse")) 13 | ) 14 | ret = await xbl_client.catalog.get_products(["C5DTJ99626K3", "BT5P2X999VH2"]) 15 | 16 | assert len(ret.products) == 2 17 | assert route.called 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_get_products_detail(respx_mock, xbl_client): 22 | route = respx_mock.get("https://displaycatalog.mp.microsoft.com").mock( 23 | return_value=Response(200, json=get_response_json("catalog_browse_details")) 24 | ) 25 | 26 | ret = await xbl_client.catalog.get_products( 27 | ["C5DTJ99626K3", "BT5P2X999VH2"], fields=FieldsTemplate.DETAILS 28 | ) 29 | 30 | assert len(ret.products) == 2 31 | assert route.called 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_get_product_from_alternate_id(respx_mock, xbl_client): 36 | route = respx_mock.get("https://displaycatalog.mp.microsoft.com").mock( 37 | return_value=Response(200, json=get_response_json("catalog_product_lookup")) 38 | ) 39 | ret = await xbl_client.catalog.get_product_from_alternate_id( 40 | "4DF9E0F8.Netflix_mcm4njqhnhss8", AlternateIdType.PACKAGE_FAMILY_NAME 41 | ) 42 | 43 | assert ret.total_result_count == 1 44 | assert route.called 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_get_product_from_alternate_id_legacy(respx_mock, xbl_client): 49 | route = respx_mock.get("https://displaycatalog.mp.microsoft.com").mock( 50 | return_value=Response( 51 | 200, json=get_response_json("catalog_product_lookup_legacy") 52 | ) 53 | ) 54 | ret = await xbl_client.catalog.get_product_from_alternate_id( 55 | "71e7df12-89e0-4dc7-a5ff-a182fc2df94f", AlternateIdType.LEGACY_XBOX_PRODUCT_ID 56 | ) 57 | 58 | assert ret.total_result_count == 1 59 | assert route.called 60 | 61 | 62 | @pytest.mark.asyncio 63 | async def test_product_search(respx_mock, xbl_client): 64 | route = respx_mock.get("https://displaycatalog.mp.microsoft.com").mock( 65 | return_value=Response(200, json=get_response_json("catalog_search")) 66 | ) 67 | ret = await xbl_client.catalog.product_search("dest") 68 | 69 | assert ret.total_result_count == 10 70 | assert route.called 71 | -------------------------------------------------------------------------------- /tests/test_cqs.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_channel_list_download(respx_mock, xbl_client): 9 | route = respx_mock.get("https://cqs.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("cqs_get_channel_list")) 11 | ) 12 | 13 | ret = await xbl_client.cqs.get_channel_list( 14 | locale_info="de-DE", headend_id="dbd2530a-fcd5-8ff0-b89d-20cd7e021502" 15 | ) 16 | 17 | assert len(ret.channels) == 8 18 | assert route.called 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_schedule_download(respx_mock, xbl_client): 23 | route = respx_mock.get("https://cqs.xboxlive.com").mock( 24 | return_value=Response(200, json=get_response_json("cqs_get_schedule")) 25 | ) 26 | 27 | ret = await xbl_client.cqs.get_schedule( 28 | locale_info="de-DE", 29 | headend_id="dbd2530a-fcd5-8ff0-b89d-20cd7e021502", 30 | start_date="2018-03-20T23:50:00.000Z", 31 | duration_minutes=60, 32 | channel_skip=0, 33 | channel_count=5, 34 | ) 35 | 36 | assert len(ret.channels) == 5 37 | assert route.called 38 | -------------------------------------------------------------------------------- /tests/test_lists.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_get_list(respx_mock, xbl_client): 9 | route = respx_mock.get("https://eplists.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("lists_get_items")) 11 | ) 12 | ret = await xbl_client.lists.get_items(xbl_client.xuid) 13 | 14 | assert ret.list_metadata.list_count == 3 15 | assert route.called 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_list_add(respx_mock, xbl_client): 20 | route = respx_mock.post("https://eplists.xboxlive.com").mock( 21 | return_value=Response(200, json=get_response_json("list_add_item")) 22 | ) 23 | post_body = { 24 | "Items": [ 25 | { 26 | "Locale": "en-US", 27 | "ContentType": "DDurable", 28 | "Title": "Destiny 2: Shadowkeep + Season", 29 | "ItemId": "361f6d1c-7d72-4b95-8481-92fdf167363f", 30 | "DeviceType": "XboxOne", 31 | "ImageUrl": r"https:\/\/store-images.s-microsoft.com\/image\/apps.47381.13678370117067710.1218a7fe-a12c-4b72-ab48-1609d37bb31e.08ee0643-ed52-4e52-9e24-1d944888baf7", 32 | } 33 | ] 34 | } 35 | ret = await xbl_client.lists.insert_items(xbl_client.xuid, post_body) 36 | 37 | assert ret.list_count == 8 38 | assert route.called 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_list_delete(respx_mock, xbl_client): 43 | route = respx_mock.delete("https://eplists.xboxlive.com").mock( 44 | return_value=Response(200, json=get_response_json("list_delete_item")) 45 | ) 46 | post_body = { 47 | "Items": [ 48 | { 49 | "Locale": "en-US", 50 | "ContentType": "DDurable", 51 | "Title": "Destiny 2: Shadowkeep + Season", 52 | "ItemId": "361f6d1c-7d72-4b95-8481-92fdf167363f", 53 | "DeviceType": "XboxOne", 54 | "ImageUrl": r"https:\/\/store-images.s-microsoft.com\/image\/apps.47381.13678370117067710.1218a7fe-a12c-4b72-ab48-1609d37bb31e.08ee0643-ed52-4e52-9e24-1d944888baf7", 55 | } 56 | ] 57 | } 58 | ret = await xbl_client.lists.remove_items(xbl_client.xuid, post_body) 59 | 60 | assert ret.list_count == 7 61 | assert route.called 62 | -------------------------------------------------------------------------------- /tests/test_mediahub.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_media_screenshots_own(respx_mock, xbl_client): 9 | route = respx_mock.post("https://mediahub.xboxlive.com/screenshots/search").mock( 10 | return_value=Response(200, json=get_response_json("mediahub_screenshots_own")) 11 | ) 12 | ret = await xbl_client.mediahub.fetch_own_screenshots() 13 | 14 | assert len(ret.values) == 1 15 | assert route.called 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_media_gameclips_own(respx_mock, xbl_client): 20 | route = respx_mock.post("https://mediahub.xboxlive.com/gameclips/search").mock( 21 | return_value=Response(200, json=get_response_json("mediahub_gameclips_own")) 22 | ) 23 | ret = await xbl_client.mediahub.fetch_own_clips() 24 | 25 | assert len(ret.values) == 1 26 | assert route.called 27 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_get_inbox(respx_mock, xbl_client): 9 | route = respx_mock.get("https://xblmessaging.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("message_get_inbox")) 11 | ) 12 | await xbl_client.message.get_inbox() 13 | 14 | assert route.called 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_get_conversation(respx_mock, xbl_client): 19 | route = respx_mock.get("https://xblmessaging.xboxlive.com").mock( 20 | return_value=Response(200, json=get_response_json("message_get_conversation")) 21 | ) 22 | await xbl_client.message.get_conversation( 23 | "05907fa3-0000-0009-acbd-299772a90900" 24 | ) 25 | 26 | assert route.called 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_get_new_conversation(respx_mock, xbl_client): 31 | route = respx_mock.get("https://xblmessaging.xboxlive.com").mock( 32 | return_value=Response(200, json=get_response_json("message_new_conversation")) 33 | ) 34 | await xbl_client.message.get_conversation( 35 | "05907fa3-0000-0009-acbd-299772a90900" 36 | ) 37 | 38 | assert route.called 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_delete_conversation(respx_mock, xbl_client): 43 | route = respx_mock.put("https://xblmessaging.xboxlive.com").mock( 44 | return_value=Response(200) 45 | ) 46 | ret = await xbl_client.message.delete_conversation( 47 | "05907fa3-0000-0009-acbd-299772a90900", "14670705998559210" 48 | ) 49 | 50 | assert ret is True 51 | assert route.called 52 | 53 | 54 | @pytest.mark.asyncio 55 | async def test_delete_message(respx_mock, xbl_client): 56 | route = respx_mock.delete("https://xblmessaging.xboxlive.com").mock( 57 | return_value=Response(200) 58 | ) 59 | ret = await xbl_client.message.delete_message( 60 | "05907fa3-0000-0009-acbd-299772a90900", "14670705998559210" 61 | ) 62 | 63 | assert ret is True 64 | assert route.called 65 | 66 | 67 | @pytest.mark.asyncio 68 | async def test_send_message(respx_mock, xbl_client): 69 | route = respx_mock.post("https://xblmessaging.xboxlive.com").mock( 70 | return_value=Response(200, json=get_response_json("message_send_message")) 71 | ) 72 | ret = await xbl_client.message.send_message("12345", "Test message") 73 | 74 | assert ret.conversation_id 75 | assert ret.message_id 76 | assert route.called 77 | 78 | 79 | @pytest.mark.asyncio 80 | async def test_message_send_too_long(xbl_client): 81 | message = "x" * 257 82 | with pytest.raises(ValueError) as err: 83 | await xbl_client.message.send_message("12345", message) 84 | 85 | assert "exceeds max length" in str(err) 86 | -------------------------------------------------------------------------------- /tests/test_people.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_people_friends_own(respx_mock, xbl_client): 9 | route = respx_mock.get("https://peoplehub.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("people_friends_own")) 11 | ) 12 | ret = await xbl_client.people.get_friends_own() 13 | 14 | assert len(ret.people) == 2 15 | assert route.called 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_people_friends_by_xuid(respx_mock, xbl_client): 20 | route = respx_mock.get("https://peoplehub.xboxlive.com").mock( 21 | return_value=Response(200, json=get_response_json("people_friends_by_xuid")) 22 | ) 23 | ret = await xbl_client.people.get_friends_by_xuid("2669321029139235") 24 | 25 | assert len(ret.people) == 2 26 | assert route.called 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_profiles_batch(respx_mock, xbl_client): 31 | route = respx_mock.post("https://peoplehub.xboxlive.com").mock( 32 | return_value=Response(200, json=get_response_json("people_batch")) 33 | ) 34 | ret = await xbl_client.people.get_friends_own_batch( 35 | ["271958441785640", "277923030577271", "266932102913935"] 36 | ) 37 | 38 | assert len(ret.people) == 3 39 | 40 | assert route.called 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_people_recommendations(respx_mock, xbl_client): 45 | route = respx_mock.get("https://peoplehub.xboxlive.com").mock( 46 | return_value=Response(200, json=get_response_json("people_recommendations")) 47 | ) 48 | ret = await xbl_client.people.get_friend_recommendations() 49 | 50 | assert ret.recommendation_summary.friend_of_friend == 20 51 | assert route.called 52 | 53 | 54 | @pytest.mark.asyncio 55 | async def test_people_summary_own(respx_mock, xbl_client): 56 | route = respx_mock.get("https://social.xboxlive.com").mock( 57 | return_value=Response(200, json=get_response_json("people_summary_own")) 58 | ) 59 | await xbl_client.people.get_friends_summary_own() 60 | 61 | assert route.called 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_people_summary_by_xuid(respx_mock, xbl_client): 66 | route = respx_mock.get("https://social.xboxlive.com").mock( 67 | return_value=Response(200, json=get_response_json("people_summary_by_xuid")) 68 | ) 69 | await xbl_client.people.get_friends_summary_by_xuid("2669321029139235") 70 | 71 | assert route.called 72 | 73 | 74 | @pytest.mark.asyncio 75 | async def test_people_summary_by_gamertag(respx_mock, xbl_client): 76 | route = respx_mock.get("https://social.xboxlive.com").mock( 77 | return_value=Response(200, json=get_response_json("people_summary_by_gamertag")) 78 | ) 79 | await xbl_client.people.get_friends_summary_by_gamertag("e") 80 | 81 | assert route.called 82 | -------------------------------------------------------------------------------- /tests/test_presence.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from xbox.webapi.api.provider.presence.models import PresenceState 5 | 6 | from tests.common import get_response_json 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_presence(respx_mock, xbl_client): 11 | route = respx_mock.get("https://userpresence.xboxlive.com").mock( 12 | return_value=Response(200, json=get_response_json("presence")) 13 | ) 14 | await xbl_client.presence.get_presence("2669321029139235") 15 | 16 | assert route.called 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_presence_batch(respx_mock, xbl_client): 21 | route = respx_mock.post("https://userpresence.xboxlive.com").mock( 22 | return_value=Response(200, json=get_response_json("presence_batch")) 23 | ) 24 | ret = await xbl_client.presence.get_presence_batch( 25 | ["2669321029139235", "2584878536129841"] 26 | ) 27 | 28 | assert len(ret) == 2 29 | assert route.called 30 | 31 | 32 | @pytest.mark.asyncio 33 | async def test_presence_too_many_people(xbl_client): 34 | xuids = range(0, 2000) 35 | with pytest.raises(Exception) as err: 36 | await xbl_client.presence.get_presence_batch(xuids) 37 | 38 | assert "length is > 1100" in str(err) 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_presence_own(respx_mock, xbl_client): 43 | route = respx_mock.get("https://userpresence.xboxlive.com").mock( 44 | return_value=Response(200, json=get_response_json("presence_own")) 45 | ) 46 | await xbl_client.presence.get_presence_own() 47 | 48 | assert route.called 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_presence_own_set(respx_mock, xbl_client): 53 | route = respx_mock.put( 54 | "https://userpresence.xboxlive.com/users/xuid(2669321029139235)/state" 55 | ).mock(return_value=Response(200)) 56 | 57 | ret = await xbl_client.presence.set_presence_own(PresenceState.ACTIVE) 58 | 59 | assert route.called 60 | assert ret 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_presence_own_set_fail(respx_mock, xbl_client): 65 | route = respx_mock.put( 66 | "https://userpresence.xboxlive.com/users/xuid(2669321029139235)/state" 67 | ).mock(return_value=Response(500)) 68 | 69 | ret = await xbl_client.presence.set_presence_own(PresenceState.CLOAKED) 70 | 71 | assert route.called 72 | assert not ret 73 | -------------------------------------------------------------------------------- /tests/test_profile.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_profile_by_xuid(respx_mock, xbl_client): 9 | route = respx_mock.get("https://profile.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("profile_by_xuid")) 11 | ) 12 | ret = await xbl_client.profile.get_profile_by_xuid("2669321029139235") 13 | 14 | assert len(ret.profile_users) == 1 15 | 16 | assert route.called 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_profile_by_gamertag(respx_mock, xbl_client): 21 | route = respx_mock.get("https://profile.xboxlive.com").mock( 22 | return_value=Response(200, json=get_response_json("profile_by_gamertag")) 23 | ) 24 | ret = await xbl_client.profile.get_profile_by_gamertag("e") 25 | 26 | assert len(ret.profile_users) == 1 27 | 28 | assert route.called 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_profiles_batch(respx_mock, xbl_client): 33 | route = respx_mock.post("https://profile.xboxlive.com").mock( 34 | return_value=Response(200, json=get_response_json("profile_batch")) 35 | ) 36 | ret = await xbl_client.profile.get_profiles( 37 | ["2669321029139235", "2584878536129841"] 38 | ) 39 | 40 | assert len(ret.profile_users) == 2 41 | 42 | assert route.called 43 | -------------------------------------------------------------------------------- /tests/test_request_signer.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from binascii import unhexlify 3 | import pytest 4 | from ecdsa.keys import VerifyingKey, BadSignatureError 5 | 6 | from xbox.webapi.common.request_signer import RequestSigner 7 | 8 | 9 | def test_synthetic_proof_key(synthetic_request_signer: RequestSigner): 10 | correct_proof = { 11 | "crv": "P-256", 12 | "alg": "ES256", 13 | "use": "sig", 14 | "kty": "EC", 15 | "x": "OKyCQ9qH5U4lZcS0c5_LxIyKvOpKe0l3x4Eg5OgDbzc", 16 | "y": "syjS0YE9vH3eBat61P9TkCpseo0qtL0weQKP-PJtIho", 17 | } 18 | assert synthetic_request_signer.proof_field == correct_proof 19 | 20 | 21 | def test_synthetic_concat(synthetic_request_signer: RequestSigner, synthetic_timestamp): 22 | ts_bytes = RequestSigner.get_timestamp_buffer(synthetic_timestamp) 23 | 24 | test_data = synthetic_request_signer._concat_data_to_sign( 25 | signature_version=b"\x00\x00\x00\x01", 26 | method="POST", 27 | path_and_query="/path?query=1", 28 | body=b"thebodygoeshere", 29 | authorization="XBL3.0 x=userid;jsonwebtoken", 30 | ts_bytes=ts_bytes, 31 | max_body_bytes=8192, 32 | ) 33 | 34 | assert ( 35 | test_data.hex() 36 | == "000000010001d6138d10f7cc8000504f5354002f706174683f71756572793d310058424c332e3020783d7573657269643b6a736f6e776562746f6b656e00746865626f6479676f65736865726500" 37 | ) 38 | 39 | 40 | def test_synthetic_hash(synthetic_request_signer: RequestSigner, synthetic_timestamp): 41 | ts_bytes = RequestSigner.get_timestamp_buffer(synthetic_timestamp) 42 | 43 | test_data = synthetic_request_signer._concat_data_to_sign( 44 | signature_version=b"\x00\x00\x00\x01", 45 | method="POST", 46 | path_and_query="/path?query=1", 47 | body=b"thebodygoeshere", 48 | authorization="XBL3.0 x=userid;jsonwebtoken", 49 | ts_bytes=ts_bytes, 50 | max_body_bytes=8192, 51 | ) 52 | 53 | test_hash = synthetic_request_signer._hash(test_data) 54 | 55 | assert ( 56 | test_hash.hex() 57 | == "f7d61b6f8d4dcd86da1aa8553f0ee7c15450811e7cd2759364e22f67d853ff50" 58 | ) 59 | 60 | 61 | def test_synthetic_signature( 62 | synthetic_request_signer: RequestSigner, synthetic_timestamp 63 | ): 64 | test_signature = synthetic_request_signer.sign( 65 | method="POST", 66 | path_and_query="/path?query=1", 67 | body=b"thebodygoeshere", 68 | authorization="XBL3.0 x=userid;jsonwebtoken", 69 | timestamp=synthetic_timestamp, 70 | ) 71 | 72 | assert ( 73 | test_signature 74 | == "AAAAAQHWE40Q98yAFe3R7GuZfvGA350cH7hWgg4HIHjaD9lGYiwxki6bNyGnB8dMEIfEmBiuNuGUfWjY5lL2h44X/VMGOkPIezVb7Q==" 75 | ) 76 | 77 | 78 | def test_synthetic_verify_digest( 79 | synthetic_request_signer: RequestSigner, ecdsa_verifying_key: VerifyingKey 80 | ): 81 | message = unhexlify( 82 | "f7d61b6f8d4dcd86da1aa8553f0ee7c15450811e7cd2759364e22f67d853ff50" 83 | ) 84 | signature = base64.b64decode( 85 | "Fe3R7GuZfvGA350cH7hWgg4HIHjaD9lGYiwxki6bNyGnB8dMEIfEmBiuNuGUfWjY5lL2h44X/VMGOkPIezVb7Q==" 86 | ) 87 | invalid_signature = b"\xFF" + bytes(signature)[1:] 88 | success = synthetic_request_signer.verify_digest(signature, message) 89 | success_via_vk = synthetic_request_signer.verify_digest( 90 | signature, message, ecdsa_verifying_key 91 | ) 92 | with pytest.raises(BadSignatureError): 93 | synthetic_request_signer.verify_digest(invalid_signature, message) 94 | 95 | assert success is True 96 | assert success_via_vk is True 97 | 98 | 99 | def test_import(ecdsa_signing_key_str: str): 100 | signer = RequestSigner.from_pem(ecdsa_signing_key_str) 101 | export = signer.export_signing_key() 102 | 103 | assert ecdsa_signing_key_str == export 104 | -------------------------------------------------------------------------------- /tests/test_signed_session.py: -------------------------------------------------------------------------------- 1 | from httpx import Request, Response 2 | import pytest 3 | 4 | from xbox.webapi.common.signed_session import SignedSession 5 | 6 | from tests.common import get_response_json 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_sending_signed_request(synthetic_request_signer, respx_mock): 11 | route = respx_mock.post("https://xsts.auth.xboxlive.com").mock( 12 | return_value=Response(200, json=get_response_json("auth_xsts_token")) 13 | ) 14 | 15 | signed_session = SignedSession(synthetic_request_signer) 16 | 17 | request = Request( 18 | method="POST", 19 | url="https://xsts.auth.xboxlive.com/xsts/authorize", 20 | headers={"x-xbl-contract-version": "1"}, 21 | data={ 22 | "RelyingParty": "http://xboxlive.com", 23 | "TokenType": "JWT", 24 | "Properties": { 25 | "UserTokens": ["eyJWTblabla"], 26 | "SandboxId": "RETAIL", 27 | }, 28 | }, 29 | ) 30 | 31 | async with signed_session: 32 | resp = await signed_session.send_request_signed(request) 33 | 34 | assert route.called 35 | assert resp.request.headers.get("Signature") is not None 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_sending_signed(synthetic_request_signer, respx_mock): 40 | route = respx_mock.post("https://xsts.auth.xboxlive.com").mock( 41 | return_value=Response(200, json=get_response_json("auth_xsts_token")) 42 | ) 43 | 44 | signed_session = SignedSession(synthetic_request_signer) 45 | 46 | method = "POST" 47 | url = "https://xsts.auth.xboxlive.com/xsts/authorize" 48 | headers = {"x-xbl-contract-version": "1"} 49 | data = { 50 | "RelyingParty": "http://xboxlive.com", 51 | "TokenType": "JWT", 52 | "Properties": { 53 | "UserTokens": ["eyJWTblabla"], 54 | "SandboxId": "RETAIL", 55 | }, 56 | } 57 | 58 | async with signed_session: 59 | resp = await signed_session.send_signed(method, url, headers=headers, data=data) 60 | 61 | assert route.called 62 | assert resp.request.headers.get("Signature") is not None 63 | -------------------------------------------------------------------------------- /tests/test_titlehub.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_titlehub_titlehistory(respx_mock, xbl_client): 9 | route = respx_mock.get("https://titlehub.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("titlehub_titlehistory")) 11 | ) 12 | ret = await xbl_client.titlehub.get_title_history(987654321) 13 | 14 | assert len(ret.titles) == 5 15 | 16 | assert route.called 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_titlehub_titleinfo(respx_mock, xbl_client): 21 | route = respx_mock.get("https://titlehub.xboxlive.com").mock( 22 | return_value=Response(200, json=get_response_json("titlehub_titleinfo")) 23 | ) 24 | ret = await xbl_client.titlehub.get_title_info(1717113201) 25 | 26 | assert len(ret.titles) == 1 27 | assert ret.titles[0].detail.genres == ["Action & adventure"] 28 | 29 | assert route.called 30 | 31 | 32 | @pytest.mark.asyncio 33 | async def test_titlehub_batch(respx_mock, xbl_client): 34 | route = respx_mock.post("https://titlehub.xboxlive.com").mock( 35 | return_value=Response(200, json=get_response_json("titlehub_batch")) 36 | ) 37 | ret = await xbl_client.titlehub.get_titles_batch( 38 | ["Microsoft.SeaofThieves_8wekyb3d8bbwe", "Microsoft.XboxApp_8wekyb3d8bbwe"] 39 | ) 40 | 41 | assert len(ret.titles) == 2 42 | 43 | assert ret.titles[0].detail.genres == [] 44 | assert ret.titles[1].detail.genres == ["Action & adventure"] 45 | 46 | assert route.called 47 | -------------------------------------------------------------------------------- /tests/test_usersearch.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_profile_by_xuid(respx_mock, xbl_client): 9 | route = respx_mock.get("https://usersearch.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("usersearch_live_search")) 11 | ) 12 | ret = await xbl_client.usersearch.get_live_search("tux") 13 | 14 | assert len(ret.results) == 8 15 | 16 | assert route.called 17 | -------------------------------------------------------------------------------- /tests/test_userstats.py: -------------------------------------------------------------------------------- 1 | from httpx import Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_userstats_by_scid(respx_mock, xbl_client): 9 | route = respx_mock.get("https://userstats.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("userstats_by_scid")) 11 | ) 12 | ret = await xbl_client.userstats.get_stats( 13 | "2669321029139235", "1370999b-fca2-4c53-8ec5-73493bcb67e5" 14 | ) 15 | 16 | assert len(ret.statlistscollection) == 1 17 | 18 | assert route.called 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_userstats_by_scid_with_metadata(respx_mock, xbl_client): 23 | route = respx_mock.get("https://userstats.xboxlive.com").mock( 24 | return_value=Response( 25 | 200, json=get_response_json("userstats_by_scid_with_metadata") 26 | ) 27 | ) 28 | ret = await xbl_client.userstats.get_stats_with_metadata( 29 | "2669321029139235", "1370999b-fca2-4c53-8ec5-73493bcb67e5" 30 | ) 31 | 32 | assert len(ret.statlistscollection) == 1 33 | 34 | assert route.called 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_userstats_batch(respx_mock, xbl_client): 39 | route = respx_mock.post("https://userstats.xboxlive.com").mock( 40 | return_value=Response(200, json=get_response_json("userstats_batch")) 41 | ) 42 | ret = await xbl_client.userstats.get_stats_batch(["2584878536129841"], "1717113201") 43 | 44 | assert len(ret.statlistscollection) == 1 45 | assert len(ret.groups) == 1 46 | assert len(ret.groups[0].statlistscollection) > 0 47 | 48 | assert route.called 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_userstats_batch_by_scid(respx_mock, xbl_client): 53 | route = respx_mock.post("https://userstats.xboxlive.com").mock( 54 | return_value=Response(200, json=get_response_json("userstats_batch_by_scid")) 55 | ) 56 | ret = await xbl_client.userstats.get_stats_batch_by_scid( 57 | ["2669321029139235"], "1370999b-fca2-4c53-8ec5-73493bcb67e5" 58 | ) 59 | 60 | assert len(ret.statlistscollection) == 1 61 | assert len(ret.groups) == 1 62 | assert len(ret.groups[0].statlistscollection) == 0 63 | 64 | assert route.called 65 | -------------------------------------------------------------------------------- /tests/test_xal.py: -------------------------------------------------------------------------------- 1 | from httpx import AsyncClient, Response 2 | import pytest 3 | 4 | from tests.common import get_response_json 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_get_title_endpoints(respx_mock, xal_mgr): 9 | route = respx_mock.get("https://title.mgt.xboxlive.com").mock( 10 | return_value=Response(200, json=get_response_json("auth_title_endpoints")) 11 | ) 12 | async with AsyncClient() as client: 13 | await xal_mgr.get_title_endpoints(client) 14 | assert route.called 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_get_device_token(respx_mock, xal_mgr): 19 | route = respx_mock.post( 20 | "https://device.auth.xboxlive.com/device/authenticate" 21 | ).mock(return_value=Response(200, json=get_response_json("auth_device_token"))) 22 | await xal_mgr.request_device_token() 23 | assert route.called 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_sisu_authentication(respx_mock, xal_mgr): 28 | route = respx_mock.post("https://sisu.xboxlive.com/authenticate").mock( 29 | return_value=Response( 30 | 200, 31 | json=get_response_json("xal_authentication_resp"), 32 | headers={"X-SessionId": "abcsession-id"}, 33 | ) 34 | ) 35 | resp, session_id = await xal_mgr.request_sisu_authentication( 36 | "eyDeviceToken", "code_challenge_string", "state_string" 37 | ) 38 | assert route.called 39 | assert session_id == "abcsession-id" 40 | assert resp.msa_oauth_redirect is not None 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_sisu_authorization(respx_mock, xal_mgr): 45 | route = respx_mock.post("https://sisu.xboxlive.com/authorize").mock( 46 | return_value=Response(200, json=get_response_json("xal_authorization_resp")) 47 | ) 48 | await xal_mgr.do_sisu_authorization( 49 | "SISU-Session-ID", "eyAccessToken", "eyDeviceToken" 50 | ) 51 | assert route.called 52 | 53 | 54 | @pytest.mark.asyncio 55 | async def test_exchange_code_for_token(respx_mock, xal_mgr): 56 | route = respx_mock.post("https://login.live.com").mock( 57 | return_value=Response(200, json=get_response_json("auth_oauth2_token")) 58 | ) 59 | await xal_mgr.exchange_code_for_token("abc", "xyz") 60 | 61 | assert route.called 62 | -------------------------------------------------------------------------------- /tests/test_xbl_client.py: -------------------------------------------------------------------------------- 1 | from xbox.webapi.api.client import XboxLiveClient 2 | 3 | 4 | def test_authorization_header(auth_mgr): 5 | client = XboxLiveClient(auth_mgr) 6 | 7 | assert ( 8 | client._auth_mgr.xsts_token.authorization_header_value 9 | == "XBL3.0 x=abcdefg;123456789" 10 | ) 11 | -------------------------------------------------------------------------------- /xbox/webapi/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for xbox-webapi-python.""" 2 | 3 | __author__ = """OpenXbox""" 4 | __version__ = "2.1.0" 5 | -------------------------------------------------------------------------------- /xbox/webapi/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/xbox/webapi/api/__init__.py -------------------------------------------------------------------------------- /xbox/webapi/api/language.py: -------------------------------------------------------------------------------- 1 | """ 2 | Language definitions 3 | """ 4 | 5 | 6 | class XboxLiveLanguage: 7 | def __init__(self, name, short_id, identifier, locale): 8 | """ 9 | Initialize a new instance of :class:`XboxLiveLanguage` 10 | 11 | Args: 12 | name (str): Full name describing the language / country 13 | short_id (str): Short Id (e.g. "AT" for Austria) 14 | identifier (str): Identifier (e.g. "de_AT" for Austria) 15 | locale (str): Locale (e.g. "de-AT" for Austria) 16 | """ 17 | self.name = name 18 | self.short_id = short_id 19 | self.identifier = identifier 20 | self.locale = locale 21 | 22 | 23 | class DefaultXboxLiveLanguages: 24 | """ 25 | Collection of locales compatible with XBL 26 | """ 27 | 28 | Argentina = XboxLiveLanguage("Argentina", "AR", "es_AR", "es-AR") 29 | Australia = XboxLiveLanguage("Australia", "AU", "en_AU", "en-AU") 30 | Austria = XboxLiveLanguage("Austria", "AT", "de_AT", "de-AT") 31 | Belgium = XboxLiveLanguage("Belgium", "BE", "fr_BE", "fr-BE") 32 | Belgium_NL = XboxLiveLanguage("Belgium (NL)", "NL", "nl_BE", "nl-BE") 33 | Brazil = XboxLiveLanguage("Brazil", "BR", "pt_BR", "pt-BR") 34 | Canada = XboxLiveLanguage("Canada", "CA", "en_CA", "en-CA") 35 | Canada_FR = XboxLiveLanguage("Canada (FR)", "CA", "fr_CA", "fr-CA") 36 | Czech_Republic = XboxLiveLanguage("Czech Republic", "CZ", "en_CZ", "en-CZ") 37 | Denmark = XboxLiveLanguage("Denmark", "DK", "da_DK", "da-DK") 38 | Finland = XboxLiveLanguage("Finland", "FI", "fi_FI", "fi-FI") 39 | France = XboxLiveLanguage("France", "FR", "fr_FR", "fr-FR") 40 | Germany = XboxLiveLanguage("Germany", "DE", "de_DE", "de-DE") 41 | Greece = XboxLiveLanguage("Greece", "GR", "en_GR", "en-GR") 42 | Hong_Kong = XboxLiveLanguage("Hong Kong", "HK", "en_HK", "en-HK") 43 | Hungary = XboxLiveLanguage("Hungary", "HU", "en_HU", "en-HU") 44 | India = XboxLiveLanguage("India", "IN", "en_IN", "en-IN") 45 | Great_Britain = XboxLiveLanguage("Great Britain", "GB", "en_GB", "en-GB") 46 | Israel = XboxLiveLanguage("Israel", "IL", "en_IL", "en-IL") 47 | Italy = XboxLiveLanguage("Italy", "IT", "it_IT", "it-IT") 48 | Japan = XboxLiveLanguage("Japan", "JP", "ja_JP", "ja-JP") 49 | Mexico = XboxLiveLanguage("Mexico", "MX", "es_MX", "es-MX") 50 | Chile = XboxLiveLanguage("Chile", "CL", "es_CL", "es-CL") 51 | Colombia = XboxLiveLanguage("Colombia", "CO", "es_CO", "es-CO") 52 | Netherlands = XboxLiveLanguage("Netherlands", "NL", "nl_NL", "nl-NL") 53 | New_Zealand = XboxLiveLanguage("New Zealand", "NZ", "en_NZ", "en-NZ") 54 | Norway = XboxLiveLanguage("Norway", "NO", "nb_NO", "nb-NO") 55 | Poland = XboxLiveLanguage("Poland", "PL", "pl_PL", "pl-PL") 56 | Portugal = XboxLiveLanguage("Portugal", "PT", "pt_PT", "pt-PT") 57 | Russia = XboxLiveLanguage("Russia", "RU", "ru_RU", "ru-RU") 58 | Saudi_Arabia = XboxLiveLanguage("Saudi Arabia", "SA", "en_SA", "en-SA") 59 | Singapore = XboxLiveLanguage("Singapore", "SG", "en_SG", "en-SG") 60 | Slovakia = XboxLiveLanguage("Slovakia", "SK", "en_SK", "en-SK") 61 | South_Africa = XboxLiveLanguage("South Afrida", "ZA", "en_ZA", "en-ZA") 62 | Korea = XboxLiveLanguage("Korea", "KR", "ko_KR", "ko-KR") 63 | Spain = XboxLiveLanguage("Spain", "ES", "es_ES", "es-ES") 64 | Switzerland = XboxLiveLanguage("Switzerland", "CH", "de_CH", "de-CH") 65 | Switzerland_FR = XboxLiveLanguage("Switzerland (FR)", "CH", "fr_CH", "fr-CH") 66 | United_Arab_Emirates = XboxLiveLanguage( 67 | "United Arab Emirates", "AE", "en_AE", "en-AE" 68 | ) 69 | United_States = XboxLiveLanguage("United States", "US", "en_US", "en-US") 70 | Ireland = XboxLiveLanguage("Ireland", "IE", "en_IE", "en-IE") 71 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/xbox/webapi/api/provider/__init__.py -------------------------------------------------------------------------------- /xbox/webapi/api/provider/account/__init__.py: -------------------------------------------------------------------------------- 1 | from xbox.webapi.api.provider.account.models import ( 2 | ChangeGamertagResult, 3 | ClaimGamertagResult, 4 | ) 5 | from xbox.webapi.api.provider.baseprovider import BaseProvider 6 | 7 | 8 | class AccountProvider(BaseProvider): 9 | BASE_URL_USER_MGT = "https://user.mgt.xboxlive.com" 10 | BASE_URL_ACCOUNT = "https://accounts.xboxlive.com" 11 | 12 | HEADERS_USER_MGT = {"x-xbl-contract-version": "1"} 13 | HEADERS_ACCOUNT = {"x-xbl-contract-version": "2"} 14 | 15 | async def claim_gamertag(self, xuid, gamertag, **kwargs) -> ClaimGamertagResult: 16 | """ 17 | Claim gamertag 18 | 19 | XLE error codes: 20 | 400 - Bad API request 21 | 401 - Unauthorized 22 | 409 - Gamertag unavailable 23 | 429 - Too many requests 24 | 200 - Gamertag available 25 | 26 | Args: 27 | xuid (int): Your xuid as integer 28 | gamertag (str): Desired gamertag 29 | 30 | Returns: ClaimGamertagResult 31 | """ 32 | url = self.BASE_URL_USER_MGT + "/gamertags/reserve" 33 | post_data = {"Gamertag": gamertag, "ReservationId": str(xuid)} 34 | resp = await self.client.session.post( 35 | url, json=post_data, headers=self.HEADERS_USER_MGT, **kwargs 36 | ) 37 | try: 38 | return ClaimGamertagResult(resp.status_code) 39 | except ValueError: 40 | resp.raise_for_status() 41 | 42 | async def change_gamertag( 43 | self, xuid, gamertag, preview=False, **kwargs 44 | ) -> ChangeGamertagResult: 45 | """ 46 | Change your gamertag. 47 | 48 | XLE error codes: 49 | 200 - success 50 | 1020 - No free gamertag changes available 51 | 52 | Args: 53 | xuid (int): Your Xuid as integer 54 | gamertag (str): Desired gamertag name 55 | preview (bool): Preview the change 56 | 57 | Returns: ChangeGamertagResult 58 | """ 59 | url = self.BASE_URL_ACCOUNT + "/users/current/profile/gamertag" 60 | post_data = { 61 | "gamertag": gamertag, 62 | "preview": preview, 63 | "reservationId": int(xuid), 64 | } 65 | resp = await self.client.session.post( 66 | url, json=post_data, headers=self.HEADERS_ACCOUNT, **kwargs 67 | ) 68 | try: 69 | return ChangeGamertagResult(resp.status_code) 70 | except ValueError: 71 | resp.raise_for_status() 72 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/account/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ClaimGamertagResult(Enum): 5 | NotAvailable = 409 6 | Available = 200 7 | 8 | 9 | class ChangeGamertagResult(Enum): 10 | ChangeSuccessful = 200 11 | NoFreeChangesAvailable = 1020 12 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/achievements/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time 2 | from typing import Any, List, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | from xbox.webapi.common.models import CamelCaseModel 7 | 8 | 9 | class PagingInfo(CamelCaseModel): 10 | continuation_token: Optional[str] = None 11 | total_records: int 12 | 13 | 14 | class Achievement360(CamelCaseModel): 15 | id: int 16 | title_id: int 17 | name: str 18 | sequence: int 19 | flags: int 20 | unlocked_online: bool 21 | unlocked: bool 22 | is_secret: bool 23 | platform: int 24 | gamerscore: int 25 | image_id: int 26 | description: str 27 | locked_description: str 28 | type: int 29 | is_revoked: bool 30 | time_unlocked: datetime 31 | 32 | 33 | class Title360(CamelCaseModel): 34 | last_played: datetime 35 | current_achievements: int 36 | current_gamerscore: int 37 | sequence: int 38 | title_id: int 39 | title_type: int 40 | platforms: List[int] 41 | name: str 42 | total_achievements: int 43 | total_gamerscore: int 44 | 45 | 46 | class Achievement360Response(CamelCaseModel): 47 | achievements: List[Achievement360] 48 | paging_info: PagingInfo 49 | version: datetime 50 | 51 | 52 | class Achievement360ProgressResponse(CamelCaseModel): 53 | titles: List[Title360] 54 | paging_info: PagingInfo 55 | version: datetime 56 | 57 | 58 | class TitleAssociation(BaseModel): 59 | name: str 60 | id: int 61 | 62 | 63 | class Requirement(CamelCaseModel): 64 | id: str 65 | current: Optional[str] = None 66 | target: str 67 | operation_type: str 68 | value_type: str 69 | rule_participation_type: str 70 | 71 | 72 | class Progression(CamelCaseModel): 73 | requirements: List[Requirement] 74 | time_unlocked: datetime 75 | 76 | 77 | class MediaAsset(BaseModel): 78 | name: str 79 | type: str 80 | url: str 81 | 82 | 83 | class Reward(CamelCaseModel): 84 | name: Any = None 85 | description: Any = None 86 | value: str 87 | type: str 88 | media_asset: Any = None 89 | value_type: str 90 | 91 | 92 | class Achievement(CamelCaseModel): 93 | id: str 94 | service_config_id: str 95 | name: str 96 | title_associations: List[TitleAssociation] 97 | progress_state: str 98 | progression: Progression 99 | media_assets: List[MediaAsset] 100 | platforms: List[str] 101 | is_secret: bool 102 | description: str 103 | locked_description: str 104 | product_id: str 105 | achievement_type: str 106 | participation_type: str 107 | time_window: Any = None 108 | rewards: List[Reward] 109 | estimated_time: time 110 | deeplink: Any = None 111 | is_revoked: bool 112 | 113 | 114 | class AchievementResponse(CamelCaseModel): 115 | achievements: List[Achievement] 116 | paging_info: PagingInfo 117 | 118 | 119 | class Title(CamelCaseModel): 120 | last_unlock: datetime 121 | title_id: int 122 | service_config_id: str 123 | title_type: str 124 | platform: str 125 | name: str 126 | earned_achievements: int 127 | current_gamerscore: int 128 | max_gamerscore: int 129 | 130 | 131 | class RecentProgressResponse(CamelCaseModel): 132 | titles: List[Title] 133 | paging_info: PagingInfo 134 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/baseprovider.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseProvider 3 | 4 | Subclassed by every *real* provider 5 | """ 6 | 7 | 8 | class BaseProvider: 9 | def __init__(self, client): 10 | """ 11 | Initialize an the BaseProvider 12 | 13 | Args: 14 | client (:class:`XboxLiveClient`): Instance of XboxLiveClient 15 | """ 16 | self.client = client 17 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/catalog/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Store Catalog - Lookup Product Information 3 | """ 4 | from typing import List 5 | 6 | from xbox.webapi.api.provider.baseprovider import BaseProvider 7 | from xbox.webapi.api.provider.catalog.models import ( 8 | AlternateIdType, 9 | CatalogResponse, 10 | CatalogSearchResponse, 11 | FieldsTemplate, 12 | PlatformType, 13 | ) 14 | 15 | 16 | class CatalogProvider(BaseProvider): 17 | CATALOG_URL = "https://displaycatalog.mp.microsoft.com" 18 | SEPERATOR = "," 19 | 20 | async def get_products( 21 | self, 22 | big_ids: List[str], 23 | fields: FieldsTemplate = FieldsTemplate.DETAILS, 24 | **kwargs, 25 | ) -> CatalogResponse: 26 | """Lookup product by Big IDs.""" 27 | ids = self.SEPERATOR.join(big_ids) 28 | params = { 29 | "actionFilter": "Browse", 30 | "bigIds": ids, 31 | "fieldsTemplate": fields.value, 32 | "languages": self.client.language.locale, 33 | "market": self.client.language.short_id, 34 | } 35 | url = f"{self.CATALOG_URL}/v7.0/products" 36 | resp = await self.client.session.get( 37 | url, params=params, include_auth=False, **kwargs 38 | ) 39 | resp.raise_for_status() 40 | return CatalogResponse(**resp.json()) 41 | 42 | async def get_product_from_alternate_id( 43 | self, 44 | id: str, 45 | id_type: AlternateIdType, 46 | fields: FieldsTemplate = FieldsTemplate.DETAILS, 47 | top: int = 25, 48 | **kwargs, 49 | ) -> CatalogResponse: 50 | """Lookup product by Alternate ID.""" 51 | params = { 52 | "top": top, 53 | "alternateId": id_type.value, 54 | "fieldsTemplate": fields.value, 55 | "languages": self.client.language.locale, 56 | "market": self.client.language.short_id, 57 | "value": id, 58 | } 59 | url = f"{self.CATALOG_URL}/v7.0/products/lookup" 60 | resp = await self.client.session.get( 61 | url, params=params, include_auth=False, **kwargs 62 | ) 63 | resp.raise_for_status() 64 | return CatalogResponse(**resp.json()) 65 | 66 | async def product_search( 67 | self, 68 | query: str, 69 | platform: PlatformType = PlatformType.XBOX, 70 | top: int = 5, 71 | **kwargs, 72 | ) -> CatalogSearchResponse: 73 | """Search for products by name.""" 74 | params = { 75 | "languages": self.client.language.locale, 76 | "market": self.client.language.short_id, 77 | "platformdependencyname": platform.value, 78 | "productFamilyNames": "Games,Apps", 79 | "query": query, 80 | "topProducts": top, 81 | } 82 | url = f"{self.CATALOG_URL}/v7.0/productFamilies/autosuggest" 83 | resp = await self.client.session.get( 84 | url, params=params, include_auth=False, **kwargs 85 | ) 86 | resp.raise_for_status() 87 | return CatalogSearchResponse(**resp.json()) 88 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/catalog/const.py: -------------------------------------------------------------------------------- 1 | """Web API Constants.""" 2 | from xbox.webapi.api.provider.catalog.models import AlternateIdType 3 | 4 | HOME_APP_IDS = { 5 | AlternateIdType.LEGACY_XBOX_PRODUCT_ID: "7b3ca835-5ef5-4d96-bc84-c1d8b5084236", 6 | AlternateIdType.XBOX_TITLE_ID: "750323071", 7 | } 8 | 9 | SYSTEM_PFN_ID_MAP = { 10 | "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe": { 11 | AlternateIdType.LEGACY_XBOX_PRODUCT_ID: "71e7df12-89e0-4dc7-a5ff-a182fc2df94f", 12 | AlternateIdType.XBOX_TITLE_ID: "371594669", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/cqs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CQS 3 | 4 | Used for download stump (TV Streaming) data 5 | (RemoteTVInput ServiceChannel on Smartglass) 6 | """ 7 | from xbox.webapi.api.provider.baseprovider import BaseProvider 8 | from xbox.webapi.api.provider.cqs.models import ( 9 | CqsChannelListResponse, 10 | CqsScheduleResponse, 11 | ) 12 | 13 | 14 | class CQSProvider(BaseProvider): 15 | CQS_URL = "https://cqs.xboxlive.com" 16 | HEADERS_CQS = { 17 | "Cache-Control": "no-cache", 18 | "Accept": "application/json", 19 | "Pragma": "no-cache", 20 | "x-xbl-client-type": "Companion", 21 | "x-xbl-client-version": "2.0", 22 | "x-xbl-contract-version": "1.b", 23 | "x-xbl-device-type": "WindowsPhone", 24 | "x-xbl-isautomated-client": "true", 25 | } 26 | 27 | async def get_channel_list( 28 | self, locale_info: str, headend_id: str, **kwargs 29 | ) -> CqsChannelListResponse: 30 | """ 31 | Get stump channel list 32 | 33 | Args: 34 | locale_info: Locale string (format: "en-US") 35 | headend_id: Headend id 36 | 37 | Returns: 38 | :class:`CqsChannelListResponse`: Channel List Response 39 | """ 40 | url = self.CQS_URL + f"/epg/{locale_info}/lineups/{headend_id}/channels" 41 | params = {"desired": "vesper_mobile_lineup"} 42 | resp = await self.client.session.get( 43 | url, params=params, headers=self.HEADERS_CQS, **kwargs 44 | ) 45 | resp.raise_for_status() 46 | return CqsChannelListResponse(**resp.json()) 47 | 48 | async def get_schedule( 49 | self, 50 | locale_info: str, 51 | headend_id: str, 52 | start_date: str, 53 | duration_minutes: int, 54 | channel_skip: int, 55 | channel_count: int, 56 | **kwargs, 57 | ) -> CqsScheduleResponse: 58 | """ 59 | Get stump epg data 60 | 61 | Args: 62 | locale_info: Locale string (format: "en-US") 63 | headend_id: Headend id 64 | start_date: Start date (format: 2016-07-11T21:50:00.000Z) 65 | duration_minutes: Schedule duration to download 66 | channel_skip: Count of channels to skip 67 | channel_count: Count of channels to get data for 68 | 69 | Returns: 70 | :class:`CqsScheduleResponse`: Schedule Response 71 | """ 72 | url = self.CQS_URL + f"/epg/{locale_info}/lineups/{headend_id}/programs" 73 | params = { 74 | "startDate": start_date, 75 | "durationMinutes": duration_minutes, 76 | "channelSkip": channel_skip, 77 | "channelCount": channel_count, 78 | "desired": "vesper_mobile_schedule", 79 | } 80 | resp = await self.client.session.get( 81 | url, params=params, headers=self.HEADERS_CQS, **kwargs 82 | ) 83 | resp.raise_for_status() 84 | return CqsScheduleResponse(**resp.json()) 85 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/cqs/models.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional 2 | 3 | from xbox.webapi.common.models import PascalCaseModel 4 | 5 | 6 | class Image(PascalCaseModel): 7 | purpose: str 8 | resize_uri: str 9 | fore_color: str 10 | 11 | 12 | class ListChannel(PascalCaseModel): 13 | id: str 14 | channel_id: str 15 | call_sign: str 16 | channel_number: str 17 | start_date: str 18 | end_date: str 19 | images: List[Image] 20 | is_HD: Optional[bool] = None 21 | 22 | 23 | class CqsChannelListResponse(PascalCaseModel): 24 | channels: List[ListChannel] 25 | 26 | 27 | class Genre(PascalCaseModel): 28 | name: str 29 | 30 | 31 | class ParentSeries(PascalCaseModel): 32 | id: str 33 | name: str 34 | 35 | 36 | class Program(PascalCaseModel): 37 | id: str 38 | media_item_type: str 39 | start_date: str 40 | end_date: str 41 | name: str 42 | is_repeat: bool 43 | parental_control: Optional[Dict[str, Any]] = None 44 | genres: List[Genre] 45 | category_id: int 46 | description: Optional[str] = None 47 | parent_series: Optional[ParentSeries] = None 48 | images: Optional[List[Image]] = None 49 | 50 | 51 | class ScheduleChannel(PascalCaseModel): 52 | id: str 53 | name: str 54 | images: List[Image] 55 | programs: List[Program] 56 | 57 | 58 | class CqsScheduleResponse(PascalCaseModel): 59 | channels: List[ScheduleChannel] 60 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/gameclips/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from xbox.webapi.common.models import CamelCaseModel 4 | 5 | 6 | class Thumbnail(CamelCaseModel): 7 | uri: str 8 | file_size: int 9 | thumbnail_type: str 10 | 11 | 12 | class GameClipUri(CamelCaseModel): 13 | uri: str 14 | file_size: int 15 | uri_type: str 16 | expiration: str 17 | 18 | 19 | class GameClip(CamelCaseModel): 20 | game_clip_id: str 21 | state: str 22 | date_published: str 23 | date_recorded: str 24 | last_modified: str 25 | user_caption: str 26 | type: str 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: str 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: Optional[str] = None 54 | 55 | 56 | class GameclipsResponse(CamelCaseModel): 57 | game_clips: List[GameClip] 58 | paging_info: PagingInfo 59 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/lists/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | EPLists - Mainly used for XBL Pins 3 | """ 4 | from xbox.webapi.api.provider.baseprovider import BaseProvider 5 | from xbox.webapi.api.provider.lists.models import ListMetadata, ListsResponse 6 | 7 | 8 | class ListsProvider(BaseProvider): 9 | LISTS_URL = "https://eplists.xboxlive.com" 10 | HEADERS_LISTS = {"Content-Type": "application/json", "x-xbl-contract-version": "2"} 11 | 12 | SEPERATOR = "." 13 | 14 | async def remove_items( 15 | self, xuid: str, post_body: dict, listname: str = "XBLPins", **kwargs 16 | ) -> ListMetadata: 17 | """ 18 | Remove items from specific list, defaults to "XBLPins" 19 | 20 | Args: 21 | xuid (str/int): Xbox User Id 22 | listname (str): Name of list to edit 23 | 24 | Returns: 25 | :class:`ListMetadata`: List Metadata Response 26 | """ 27 | url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}" 28 | resp = await self.client.session.delete( 29 | url, json=post_body, headers=self.HEADERS_LISTS, **kwargs 30 | ) 31 | resp.raise_for_status() 32 | return ListMetadata(**resp.json()) 33 | 34 | async def get_items( 35 | self, xuid: str, listname: str = "XBLPins", **kwargs 36 | ) -> ListsResponse: 37 | """ 38 | Get items from specific list, defaults to "XBLPins" 39 | 40 | Args: 41 | xuid (str/int): Xbox User Id 42 | listname (str): Name of list to edit 43 | 44 | Returns: 45 | :class:`ListsResponse`: List Response 46 | """ 47 | url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}" 48 | resp = await self.client.session.get(url, headers=self.HEADERS_LISTS, **kwargs) 49 | resp.raise_for_status() 50 | return ListsResponse(**resp.json()) 51 | 52 | async def insert_items( 53 | self, xuid: str, post_body: dict, listname: str = "XBLPins", **kwargs 54 | ) -> ListMetadata: 55 | """ 56 | Insert items to specific list, defaults to "XBLPins" 57 | 58 | Args: 59 | xuid (str/int): Xbox User Id 60 | listname (str): Name of list to edit 61 | 62 | Returns: 63 | :class:`ListMetadata`: List Metadata Response 64 | """ 65 | url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}" 66 | resp = await self.client.session.post( 67 | url, json=post_body, headers=self.HEADERS_LISTS, **kwargs 68 | ) 69 | resp.raise_for_status() 70 | return ListMetadata(**resp.json()) 71 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/lists/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from xbox.webapi.common.models import PascalCaseModel 4 | 5 | 6 | class Item(PascalCaseModel): 7 | item_id: str 8 | content_type: str 9 | title: Optional[str] = None 10 | device_type: str 11 | provider: Optional[str] = None 12 | provider_id: Optional[str] = None 13 | 14 | 15 | class ListItem(PascalCaseModel): 16 | date_added: str 17 | date_modified: str 18 | index: int 19 | k_value: int 20 | item: Item 21 | 22 | 23 | class ListMetadata(PascalCaseModel): 24 | list_title: str 25 | list_version: int 26 | list_count: int 27 | allow_duplicates: bool 28 | max_list_size: int 29 | access_setting: str 30 | 31 | 32 | class ListsResponse(PascalCaseModel): 33 | impression_id: str 34 | list_items: List[ListItem] 35 | list_metadata: ListMetadata 36 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/mediahub/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mediahub - Fetch screenshots and gameclips 3 | """ 4 | from xbox.webapi.api.provider.baseprovider import BaseProvider 5 | from xbox.webapi.api.provider.mediahub.models import ( 6 | MediahubGameclips, 7 | MediahubScreenshots, 8 | ) 9 | 10 | 11 | class MediahubProvider(BaseProvider): 12 | MEDIAHUB_URL = "https://mediahub.xboxlive.com" 13 | HEADERS = {"x-xbl-contract-version": "3"} 14 | 15 | async def fetch_own_clips( 16 | self, skip: int = 0, count: int = 500, **kwargs 17 | ) -> MediahubGameclips: 18 | """ 19 | Fetch own clips 20 | 21 | Args: 22 | skip: Number of items to skip 23 | count: Max entries to fetch 24 | 25 | Returns: 26 | :class:`MediahubGameclips`: Gameclips 27 | """ 28 | url = f"{self.MEDIAHUB_URL}/gameclips/search" 29 | post_data = { 30 | "max": count, 31 | "query": f"OwnerXuid eq {self.client.xuid}", 32 | "skip": skip, 33 | } 34 | resp = await self.client.session.post( 35 | url, json=post_data, headers=self.HEADERS, **kwargs 36 | ) 37 | resp.raise_for_status() 38 | return MediahubGameclips(**resp.json()) 39 | 40 | async def fetch_own_screenshots( 41 | self, skip: int = 0, count: int = 500, **kwargs 42 | ) -> MediahubScreenshots: 43 | """ 44 | Fetch own screenshots 45 | 46 | Args: 47 | skip: Number of items to skip 48 | count: Max entries to fetch 49 | 50 | Returns: 51 | :class:`MediahubScreenshots`: Screenshots 52 | """ 53 | url = f"{self.MEDIAHUB_URL}/screenshots/search" 54 | post_data = { 55 | "max": count, 56 | "query": f"OwnerXuid eq {self.client.xuid}", 57 | "skip": skip, 58 | } 59 | resp = await self.client.session.post( 60 | url, json=post_data, headers=self.HEADERS, **kwargs 61 | ) 62 | resp.raise_for_status() 63 | return MediahubScreenshots(**resp.json()) 64 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/mediahub/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from xbox.webapi.common.models import CamelCaseModel 4 | 5 | 6 | class ContentSegment(CamelCaseModel): 7 | segment_id: int 8 | creation_type: str 9 | creator_channel_id: Optional[str] = None 10 | creator_xuid: int 11 | record_date: str 12 | duration_in_seconds: int 13 | offset: int 14 | secondary_title_id: Optional[int] = None 15 | title_id: int 16 | 17 | 18 | class ContentLocator(CamelCaseModel): 19 | expiration: Optional[str] = None 20 | file_size: Optional[int] = None 21 | locator_type: str 22 | uri: str 23 | 24 | 25 | class GameclipContent(CamelCaseModel): 26 | content_id: str 27 | content_locators: List[ContentLocator] 28 | content_segments: List[ContentSegment] 29 | creation_type: str 30 | duration_in_seconds: int 31 | local_id: str 32 | owner_xuid: int 33 | sandbox_id: str 34 | shared_to: List[int] 35 | title_id: int 36 | title_name: str 37 | upload_date: str 38 | upload_language: str 39 | upload_region: str 40 | upload_title_id: int 41 | upload_device_type: str 42 | comment_count: int 43 | like_count: int 44 | share_count: int 45 | view_count: int 46 | content_state: str 47 | enforcement_state: str 48 | sessions: List[str] 49 | tournaments: List[str] 50 | 51 | 52 | class MediahubGameclips(CamelCaseModel): 53 | values: List[GameclipContent] 54 | 55 | 56 | class ScreenshotContent(CamelCaseModel): 57 | content_id: str 58 | capture_date: str 59 | content_locators: List[ContentLocator] 60 | local_id: str 61 | owner_xuid: int 62 | resolution_height: int 63 | resolution_width: int 64 | date_uploaded: str 65 | sandbox_id: str 66 | shared_to: List[int] 67 | title_id: int 68 | title_name: str 69 | upload_language: str 70 | upload_region: str 71 | upload_title_id: int 72 | upload_device_type: str 73 | comment_count: int 74 | like_count: int 75 | share_count: int 76 | view_count: int 77 | content_state: str 78 | enforcement_state: str 79 | sessions: List[str] 80 | tournaments: List[str] 81 | 82 | 83 | class MediahubScreenshots(CamelCaseModel): 84 | values: List[ScreenshotContent] 85 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/message/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, List, Optional 3 | 4 | from xbox.webapi.common.models import CamelCaseModel 5 | 6 | 7 | class Part(CamelCaseModel): 8 | content_type: str 9 | version: int 10 | text: Optional[str] = None 11 | unsuitable_for: Optional[List] = None 12 | locator: Optional[str] = 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: Optional[ContentPayload] = 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: Optional[int] = 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: Optional[List[str]] = None 83 | read_horizon: str 84 | delete_horizon: str 85 | is_read: bool 86 | muted: bool 87 | folder: str 88 | messages: Optional[List[Message]] = None 89 | continuation_token: Optional[str] = None 90 | voice_id: str 91 | voice_roster: Optional[List[Any]] = None 92 | 93 | 94 | class SendMessageResponse(CamelCaseModel): 95 | message_id: str 96 | conversation_id: str 97 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/presence/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Presence - Get online status of friends 3 | """ 4 | from typing import List 5 | 6 | from xbox.webapi.api.provider.baseprovider import BaseProvider 7 | from xbox.webapi.api.provider.presence.models import ( 8 | PresenceBatchResponse, 9 | PresenceItem, 10 | PresenceLevel, 11 | PresenceState, 12 | ) 13 | 14 | 15 | class PresenceProvider(BaseProvider): 16 | PRESENCE_URL = "https://userpresence.xboxlive.com" 17 | HEADERS_PRESENCE = {"x-xbl-contract-version": "3", "Accept": "application/json"} 18 | 19 | async def get_presence( 20 | self, 21 | xuid, 22 | presence_level: PresenceLevel = PresenceLevel.USER, 23 | **kwargs, 24 | ) -> PresenceItem: 25 | """ 26 | Get presence for given xuid 27 | 28 | Args: 29 | xuid: XUID 30 | presence_level: Filter level 31 | 32 | Returns: 33 | :class:`PresenceItem`: Presence Response 34 | """ 35 | url = self.PRESENCE_URL + "/users/xuid(" + xuid + ")?level=" + presence_level 36 | 37 | resp = await self.client.session.get( 38 | url, headers=self.HEADERS_PRESENCE, **kwargs 39 | ) 40 | resp.raise_for_status() 41 | return PresenceItem(**resp.json()) 42 | 43 | async def get_presence_batch( 44 | self, 45 | xuids: List[str], 46 | online_only: bool = False, 47 | presence_level: PresenceLevel = PresenceLevel.USER, 48 | **kwargs, 49 | ) -> List[PresenceItem]: 50 | """ 51 | Get presence for list of xuids 52 | 53 | Args: 54 | xuids: List of XUIDs 55 | online_only: Only get online profiles 56 | presence_level: Filter level 57 | 58 | Returns: List[:class:`PresenceItem`]: List of presence items 59 | """ 60 | if len(xuids) > 1100: 61 | raise Exception("Xuid list length is > 1100") 62 | 63 | url = self.PRESENCE_URL + "/users/batch" 64 | post_data = { 65 | "users": [str(x) for x in xuids], 66 | "onlineOnly": online_only, 67 | "level": presence_level, 68 | } 69 | resp = await self.client.session.post( 70 | url, json=post_data, headers=self.HEADERS_PRESENCE, **kwargs 71 | ) 72 | resp.raise_for_status() 73 | parsed = PresenceBatchResponse.model_validate(resp.json()) 74 | return parsed.root 75 | 76 | async def get_presence_own( 77 | self, presence_level: PresenceLevel = PresenceLevel.ALL, **kwargs 78 | ) -> PresenceItem: 79 | """ 80 | Get presence of own profile 81 | 82 | Args: 83 | presence_level: Filter level 84 | 85 | Returns: 86 | :class:`PresenceItem`: Presence Response 87 | """ 88 | url = self.PRESENCE_URL + "/users/me" 89 | params = {"level": presence_level} 90 | resp = await self.client.session.get( 91 | url, params=params, headers=self.HEADERS_PRESENCE, **kwargs 92 | ) 93 | resp.raise_for_status() 94 | return PresenceItem(**resp.json()) 95 | 96 | async def set_presence_own(self, presence_state: PresenceState, **kwargs) -> bool: 97 | """ 98 | Set presence of own profile 99 | 100 | Args: 101 | presence_state: State of presence 102 | 103 | Returns: 104 | `True` on success, `False` otherwise 105 | """ 106 | url = self.PRESENCE_URL + f"/users/xuid({self.client.xuid})/state" 107 | data = {"state": presence_state.value} 108 | resp = await self.client.session.put( 109 | url, json=data, headers=self.HEADERS_PRESENCE, **kwargs 110 | ) 111 | return resp.status_code == 200 112 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/presence/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Optional 3 | from pydantic import RootModel 4 | 5 | from xbox.webapi.common.models import CamelCaseModel 6 | 7 | 8 | class PresenceLevel(str, Enum): 9 | USER = "user" 10 | DEVICE = "device" 11 | TITLE = "title" 12 | ALL = "all" 13 | 14 | 15 | class PresenceState(str, Enum): 16 | ACTIVE = "Active" 17 | CLOAKED = "Cloaked" 18 | 19 | 20 | class LastSeen(CamelCaseModel): 21 | device_type: str 22 | title_id: Optional[str] = None 23 | title_name: str 24 | timestamp: str 25 | 26 | 27 | class ActivityRecord(CamelCaseModel): 28 | richPresence: Optional[str] = None 29 | media: Optional[str] = None 30 | 31 | 32 | class TitleRecord(CamelCaseModel): 33 | id: Optional[str] = None 34 | name: Optional[str] = None 35 | activity: Optional[List[ActivityRecord]] = None 36 | lastModified: Optional[str] = None 37 | placement: Optional[str] = None 38 | state: Optional[str] = None 39 | 40 | 41 | class DeviceRecord(CamelCaseModel): 42 | titles: Optional[List[TitleRecord]] = None 43 | type: Optional[str] = None 44 | 45 | 46 | class PresenceItem(CamelCaseModel): 47 | xuid: str 48 | state: str 49 | last_seen: Optional[LastSeen] = None 50 | devices: Optional[List[DeviceRecord]] = None 51 | 52 | 53 | class PresenceBatchResponse(RootModel[List[PresenceItem]], CamelCaseModel): 54 | root: List[PresenceItem] 55 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/profile/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | 4 | from xbox.webapi.common.models import CamelCaseModel 5 | 6 | 7 | class ProfileSettings(str, Enum): 8 | """ 9 | Profile settings, used as parameter for Profile API 10 | """ 11 | 12 | GAME_DISPLAY_NAME = "GameDisplayName" 13 | APP_DISPLAY_NAME = "AppDisplayName" 14 | APP_DISPLAYPIC_RAW = "AppDisplayPicRaw" 15 | GAME_DISPLAYPIC_RAW = "GameDisplayPicRaw" 16 | PUBLIC_GAMERPIC = "PublicGamerpic" 17 | SHOW_USER_AS_AVATAR = "ShowUserAsAvatar" 18 | GAMERSCORE = "Gamerscore" 19 | GAMERTAG = "Gamertag" 20 | MODERN_GAMERTAG = "ModernGamertag" 21 | MODERN_GAMERTAG_SUFFIX = "ModernGamertagSuffix" 22 | UNIQUE_MODERN_GAMERTAG = "UniqueModernGamertag" 23 | ACCOUNT_TIER = "AccountTier" 24 | TENURE_LEVEL = "TenureLevel" 25 | XBOX_ONE_REP = "XboxOneRep" 26 | PREFERRED_COLOR = "PreferredColor" 27 | LOCATION = "Location" 28 | BIOGRAPHY = "Bio" 29 | WATERMARKS = "Watermarks" 30 | REAL_NAME = "RealName" 31 | REAL_NAME_OVERRIDE = "RealNameOverride" 32 | IS_QUARANTINED = "IsQuarantined" 33 | 34 | 35 | class Setting(CamelCaseModel): 36 | id: str 37 | value: str 38 | 39 | 40 | class ProfileUser(CamelCaseModel): 41 | id: str 42 | host_id: str 43 | settings: List[Setting] 44 | is_sponsored_user: bool 45 | 46 | 47 | class ProfileResponse(CamelCaseModel): 48 | profile_users: List[ProfileUser] 49 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/ratelimitedprovider.py: -------------------------------------------------------------------------------- 1 | """ 2 | RateLimitedProvider 3 | 4 | Subclassed by providers with rate limit support 5 | """ 6 | 7 | from typing import Dict, Union 8 | 9 | from xbox.webapi.api.provider.baseprovider import BaseProvider 10 | from xbox.webapi.common.exceptions import XboxException 11 | from xbox.webapi.common.ratelimits import CombinedRateLimit 12 | from xbox.webapi.common.ratelimits.models import LimitType, ParsedRateLimit, TimePeriod 13 | 14 | 15 | class RateLimitedProvider(BaseProvider): 16 | # dict -> Dict (typing.dict) https://stackoverflow.com/a/63460173 17 | RATE_LIMITS: Dict[str, Union[int, Dict[str, int]]] 18 | 19 | def __init__(self, client): 20 | """ 21 | Initialize Baseclass 22 | 23 | Args: 24 | client (:class:`XboxLiveClient`): Instance of XboxLiveClient 25 | """ 26 | super().__init__(client) 27 | 28 | # Check that RATE_LIMITS set defined in the child class 29 | if hasattr(self, "RATE_LIMITS"): 30 | # Note: we cannot check (type(self.RATE_LIMITS) == dict) as the type hints have already defined it as such 31 | if "burst" and "sustain" in self.RATE_LIMITS: 32 | # We have the required keys, attempt to parse. 33 | # (type-checking for the values is performed in __parse_rate_limit_key) 34 | self.__handle_rate_limit_setup() 35 | else: 36 | raise XboxException( 37 | "RATE_LIMITS object missing required keys 'burst', 'sustain'" 38 | ) 39 | else: 40 | raise XboxException( 41 | "RateLimitedProvider as parent class but RATE_LIMITS not set!" 42 | ) 43 | 44 | def __handle_rate_limit_setup(self): 45 | # Retrieve burst and sustain from the dict 46 | burst_key = self.RATE_LIMITS["burst"] 47 | sustain_key = self.RATE_LIMITS["sustain"] 48 | 49 | # Parse the rate limit dict values 50 | burst_rate_limits = self.__parse_rate_limit_key(burst_key, TimePeriod.BURST) 51 | sustain_rate_limits = self.__parse_rate_limit_key( 52 | sustain_key, TimePeriod.SUSTAIN 53 | ) 54 | 55 | # Instanciate CombinedRateLimits for read and write respectively 56 | self.rate_limit_read = CombinedRateLimit( 57 | burst_rate_limits, sustain_rate_limits, type=LimitType.READ 58 | ) 59 | self.rate_limit_write = CombinedRateLimit( 60 | burst_rate_limits, sustain_rate_limits, type=LimitType.WRITE 61 | ) 62 | 63 | def __parse_rate_limit_key( 64 | self, key: Union[int, Dict[str, int]], period: TimePeriod 65 | ) -> ParsedRateLimit: 66 | if isinstance(key, int) and not isinstance(key, bool): 67 | # bool is a subclass of int, hence the explicit check 68 | return ParsedRateLimit(read=key, write=key, period=period) 69 | elif isinstance(key, dict): 70 | # TODO: schema here? 71 | # Since the key-value pairs match we can just pass the dict to the model 72 | return ParsedRateLimit(**key, period=period) 73 | # return ParsedRateLimit(read=key["read"], write=key["write"]) 74 | else: 75 | raise XboxException( 76 | "RATE_LIMITS value types not recognised. Must be one of 'int, 'dict'." 77 | ) 78 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/screenshots/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, List, Optional 3 | 4 | from xbox.webapi.common.models import CamelCaseModel 5 | 6 | 7 | class Thumbnail(CamelCaseModel): 8 | uri: str 9 | file_size: int 10 | thumbnail_type: str 11 | 12 | 13 | class ScreenshotUri(CamelCaseModel): 14 | uri: str 15 | file_size: int 16 | uri_type: str 17 | expiration: datetime 18 | 19 | 20 | class Screenshot(CamelCaseModel): 21 | screenshot_id: str 22 | resolution_height: int 23 | resolution_width: int 24 | state: str 25 | date_published: datetime 26 | date_taken: datetime 27 | last_modified: datetime 28 | user_caption: str 29 | type: str 30 | scid: str 31 | title_id: int 32 | rating: float 33 | rating_count: int 34 | views: int 35 | title_data: str 36 | system_properties: str 37 | saved_by_user: bool 38 | achievement_id: str 39 | greatest_moment_id: Any = None 40 | thumbnails: List[Thumbnail] 41 | screenshot_uris: List[ScreenshotUri] 42 | xuid: str 43 | screenshot_name: str 44 | title_name: str 45 | screenshot_locale: str 46 | screenshot_content_attributes: str 47 | device_type: str 48 | 49 | 50 | class PagingInfo(CamelCaseModel): 51 | continuation_token: Optional[str] = None 52 | 53 | 54 | class ScreenshotResponse(CamelCaseModel): 55 | screenshots: List[Screenshot] 56 | paging_info: PagingInfo 57 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/titlehub/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | from typing import Any, List, Optional 4 | 5 | from xbox.webapi.common.models import CamelCaseModel, PascalCaseModel 6 | 7 | 8 | class TitleFields(str, Enum): 9 | SERVICE_CONFIG_ID = "scid" 10 | ACHIEVEMENT = "achievement" 11 | STATS = "stats" 12 | GAME_PASS = "gamepass" 13 | IMAGE = "image" 14 | DETAIL = "detail" 15 | FRIENDS_WHO_PLAYED = "friendswhoplayed" 16 | ALTERNATE_TITLE_ID = "alternateTitleId" 17 | PRODUCT_ID = "productId" 18 | CONTENT_BOARD = "contentBoard" 19 | 20 | 21 | class Achievement(CamelCaseModel): 22 | current_achievements: int 23 | total_achievements: int 24 | current_gamerscore: int 25 | total_gamerscore: int 26 | progress_percentage: float 27 | source_version: int 28 | 29 | 30 | class Stats(CamelCaseModel): 31 | source_version: int 32 | 33 | 34 | class GamePass(CamelCaseModel): 35 | is_game_pass: bool 36 | 37 | 38 | class Image(CamelCaseModel): 39 | url: str 40 | type: str 41 | 42 | 43 | class TitleHistory(CamelCaseModel): 44 | last_time_played: datetime 45 | visible: bool 46 | can_hide: bool 47 | 48 | 49 | class Attribute(CamelCaseModel): 50 | applicable_platforms: Optional[List[str]] = None 51 | maximum: Optional[int] = None 52 | minimum: Optional[int] = None 53 | name: str 54 | 55 | 56 | class Availability(PascalCaseModel): 57 | actions: List[str] 58 | availability_id: str 59 | platforms: List[str] 60 | sku_id: str 61 | 62 | 63 | class Detail(CamelCaseModel): 64 | attributes: List[Attribute] 65 | availabilities: List[Availability] 66 | capabilities: List[str] 67 | description: str 68 | developer_name: str 69 | genres: Optional[List[str]] = None 70 | publisher_name: str 71 | min_age: Optional[int] = None 72 | release_date: Optional[datetime] = None 73 | short_description: Optional[str] = None 74 | vui_display_name: Optional[str] = None 75 | xbox_live_gold_required: bool 76 | 77 | 78 | class Title(CamelCaseModel): 79 | title_id: str 80 | pfn: Optional[str] = None 81 | bing_id: Optional[str] = None 82 | service_config_id: Optional[str] = None 83 | windows_phone_product_id: Optional[str] = None 84 | name: str 85 | type: str 86 | devices: List[str] 87 | display_image: str 88 | media_item_type: str 89 | modern_title_id: Optional[str] = None 90 | is_bundle: bool 91 | achievement: Optional[Achievement] = None 92 | stats: Optional[Stats] = None 93 | game_pass: Optional[GamePass] = None 94 | images: Optional[List[Image]] = None 95 | title_history: Optional[TitleHistory] = None 96 | detail: Optional[Detail] = None 97 | friends_who_played: Any = None 98 | alternate_title_ids: Any = None 99 | content_boards: Any = None 100 | xbox_live_tier: Optional[str] = None 101 | is_streamable: Optional[bool] = None 102 | 103 | 104 | class TitleHubResponse(CamelCaseModel): 105 | xuid: Optional[str] = None 106 | titles: List[Title] 107 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/usersearch/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usersearch - Search for gamertags / userprofiles 3 | """ 4 | from xbox.webapi.api.provider.baseprovider import BaseProvider 5 | from xbox.webapi.api.provider.usersearch.models import UserSearchResponse 6 | 7 | 8 | class UserSearchProvider(BaseProvider): 9 | USERSEARCH_URL = "https://usersearch.xboxlive.com" 10 | HEADERS_USER_SEARCH = {"x-xbl-contract-version": "1"} 11 | 12 | async def get_live_search(self, query: str, **kwargs) -> UserSearchResponse: 13 | """ 14 | Get userprofiles for search query 15 | 16 | Args: 17 | query: Search query 18 | 19 | Returns: 20 | :class:`UserSearchResponse`: User Search Response 21 | """ 22 | url = self.USERSEARCH_URL + "/suggest" 23 | params = {"q": query} 24 | resp = await self.client.session.get( 25 | url, params=params, headers=self.HEADERS_USER_SEARCH, **kwargs 26 | ) 27 | resp.raise_for_status() 28 | return UserSearchResponse(**resp.json()) 29 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/usersearch/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from xbox.webapi.common.models import CamelCaseModel 4 | 5 | 6 | class UserDetail(CamelCaseModel): 7 | id: str 8 | gamertag: str 9 | display_pic_uri: str 10 | score: float 11 | 12 | 13 | class UserResult(CamelCaseModel): 14 | text: str 15 | result: UserDetail 16 | 17 | 18 | class UserSearchResponse(CamelCaseModel): 19 | results: List[UserResult] 20 | -------------------------------------------------------------------------------- /xbox/webapi/api/provider/userstats/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from xbox.webapi.common.models import LowerCaseModel, PascalCaseModel 4 | 5 | 6 | class GeneralStatsField: 7 | MINUTES_PLAYED = "MinutesPlayed" 8 | 9 | 10 | class GroupProperties(PascalCaseModel): 11 | ordinal: Optional[str] = None 12 | sort_order: Optional[str] = None 13 | display_name: Optional[str] = None 14 | display_format: Optional[str] = None 15 | display_semantic: Optional[str] = None 16 | 17 | 18 | class Properties(PascalCaseModel): 19 | display_name: Optional[str] = None 20 | 21 | 22 | class Stat(LowerCaseModel): 23 | group_properties: Optional[GroupProperties] = None 24 | xuid: str 25 | scid: str 26 | name: str 27 | type: str 28 | value: str 29 | properties: Properties 30 | 31 | 32 | class StatListsCollectionItem(LowerCaseModel): 33 | arrange_by_field: str 34 | arrange_by_field_id: str 35 | stats: List[Stat] 36 | 37 | 38 | class Group(LowerCaseModel): 39 | name: str 40 | title_id: Optional[str] = None 41 | statlistscollection: List[StatListsCollectionItem] 42 | 43 | 44 | class UserStatsResponse(LowerCaseModel): 45 | groups: Optional[List[Group]] = None 46 | statlistscollection: List[StatListsCollectionItem] 47 | -------------------------------------------------------------------------------- /xbox/webapi/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/xbox/webapi/authentication/__init__.py -------------------------------------------------------------------------------- /xbox/webapi/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-webapi-python/b5c56aade7829eb4f817d7b1cf791c40583e81e8/xbox/webapi/common/__init__.py -------------------------------------------------------------------------------- /xbox/webapi/common/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Special Exception subclasses 3 | """ 4 | 5 | 6 | from xbox.webapi.common.ratelimits import RateLimit 7 | 8 | 9 | class XboxException(Exception): 10 | """Base exception for all Xbox exceptions to subclass""" 11 | 12 | pass 13 | 14 | 15 | class AuthenticationException(XboxException): 16 | """Raised when logging in fails, likely due to incorrect auth credentials""" 17 | 18 | pass 19 | 20 | 21 | class TwoFactorAuthRequired(XboxException): 22 | def __init__(self, message, server_data): 23 | """ 24 | Raised when 2FA is required 25 | 26 | Args: 27 | message (str): Exception message 28 | server_data (dict): Server data dict, extracted js object from windows live auth request 29 | """ 30 | super().__init__(message) 31 | self.server_data = server_data 32 | 33 | 34 | class InvalidRequest(XboxException): 35 | def __init__(self, message, response): 36 | """ 37 | Raised when something is wrong with the request 38 | 39 | Args: 40 | message (str): error message returned by the server 41 | response (requests.Response): Instance of :class:`requests.Response` 42 | 43 | """ 44 | self.message = message 45 | self.response = response 46 | 47 | 48 | class NotFoundException(XboxException): 49 | """Any exception raised due to a resource being missing will subclass this""" 50 | 51 | pass 52 | 53 | 54 | class RateLimitExceededException(XboxException): 55 | def __init__(self, message, rate_limit: RateLimit): 56 | self.message = message 57 | self.rate_limit = rate_limit 58 | self.try_again_in = rate_limit.get_reset_after() 59 | -------------------------------------------------------------------------------- /xbox/webapi/common/filetimes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009, David Buxton 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 15 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 16 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | """Tools to convert between Python datetime instances and Microsoft times. 26 | """ 27 | from calendar import timegm 28 | from datetime import datetime, timezone, tzinfo, timedelta 29 | 30 | # http://support.microsoft.com/kb/167296 31 | # How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME 32 | EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as MS file time 33 | HUNDREDS_OF_NANOSECONDS = 10000000 34 | 35 | 36 | ZERO = timedelta(0) 37 | HOUR = timedelta(hours=1) 38 | 39 | 40 | class UTC(tzinfo): 41 | """UTC""" 42 | 43 | def utcoffset(self, dt: datetime) -> timedelta: 44 | return ZERO 45 | 46 | def tzname(self, dt: datetime) -> str: 47 | return "UTC" 48 | 49 | def dst(self, dt: datetime) -> timedelta: 50 | return ZERO 51 | 52 | 53 | utc = UTC() 54 | 55 | 56 | def dt_to_filetime(dt: datetime) -> int: 57 | """Converts a datetime to Microsoft filetime format. If the object is 58 | time zone-naive, it is forced to UTC before conversion. 59 | 60 | >>> "%.0f" % dt_to_filetime(datetime(2009, 7, 25, 23, 0)) 61 | '128930364000000000' 62 | 63 | >>> "%.0f" % dt_to_filetime(datetime(1970, 1, 1, 0, 0, tzinfo=utc)) 64 | '116444736000000000' 65 | 66 | >>> "%.0f" % dt_to_filetime(datetime(1970, 1, 1, 0, 0)) 67 | '116444736000000000' 68 | 69 | >>> dt_to_filetime(datetime(2009, 7, 25, 23, 0, 0, 100)) 70 | 128930364000001000 71 | """ 72 | if (dt.tzinfo is None) or (dt.tzinfo.utcoffset(dt) is None): 73 | dt = dt.replace(tzinfo=utc) 74 | ft = EPOCH_AS_FILETIME + (timegm(dt.timetuple()) * HUNDREDS_OF_NANOSECONDS) 75 | return ft + (dt.microsecond * 10) 76 | 77 | 78 | def filetime_to_dt(ft: int) -> datetime: 79 | """Converts a Microsoft filetime number to a Python datetime. The new 80 | datetime object is time zone-naive but is equivalent to tzinfo=utc. 81 | 82 | >>> filetime_to_dt(116444736000000000) 83 | datetime.datetime(1970, 1, 1, 0, 0) 84 | 85 | >>> filetime_to_dt(128930364000000000) 86 | datetime.datetime(2009, 7, 25, 23, 0) 87 | 88 | >>> filetime_to_dt(128930364000001000) 89 | datetime.datetime(2009, 7, 25, 23, 0, 0, 100) 90 | """ 91 | # Get seconds and remainder in terms of Unix epoch 92 | (s, ns100) = divmod(ft - EPOCH_AS_FILETIME, HUNDREDS_OF_NANOSECONDS) 93 | # Convert to datetime object 94 | dt = datetime.fromtimestamp(s, timezone.utc) 95 | # Add remainder in as microseconds. Python 3.2 requires an integer 96 | dt = dt.replace(microsecond=(ns100 // 10)) 97 | return dt 98 | -------------------------------------------------------------------------------- /xbox/webapi/common/models.py: -------------------------------------------------------------------------------- 1 | """Base Models.""" 2 | from pydantic import ConfigDict, BaseModel 3 | 4 | 5 | def to_pascal(string): 6 | return "".join(word.capitalize() for word in string.split("_")) 7 | 8 | 9 | def to_camel(string): 10 | words = string.split("_") 11 | return words[0] + "".join(word.capitalize() for word in words[1:]) 12 | 13 | 14 | def to_lower(string): 15 | return string.replace("_", "") 16 | 17 | 18 | class PascalCaseModel(BaseModel): 19 | model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, alias_generator=to_pascal) 20 | 21 | 22 | class CamelCaseModel(BaseModel): 23 | model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, alias_generator=to_camel) 24 | 25 | 26 | class LowerCaseModel(BaseModel): 27 | model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, alias_generator=to_lower) 28 | -------------------------------------------------------------------------------- /xbox/webapi/common/ratelimits/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pydantic import BaseModel 3 | 4 | 5 | class TimePeriod(Enum): 6 | BURST = 15 # 15 seconds 7 | SUSTAIN = 300 # 5 minutes (300s) 8 | 9 | 10 | class LimitType(Enum): 11 | WRITE = 0 12 | READ = 1 13 | 14 | 15 | class IncrementResult(BaseModel): 16 | counter: int 17 | exceeded: bool 18 | 19 | 20 | class ParsedRateLimit(BaseModel): 21 | read: int 22 | write: int 23 | period: TimePeriod 24 | -------------------------------------------------------------------------------- /xbox/webapi/common/signed_session.py: -------------------------------------------------------------------------------- 1 | """ 2 | Signed Session 3 | A wrapper around httpx' AsyncClient which transparently calculates the "Signature" header. 4 | """ 5 | 6 | import httpx 7 | 8 | from ssl import SSLContext 9 | from xbox.webapi.common.request_signer import RequestSigner 10 | 11 | 12 | class SignedSession(httpx.AsyncClient): 13 | def __init__(self, request_signer=None, ssl_context: SSLContext=None): 14 | super().__init__(verify=ssl_context if ssl_context is not None else True) 15 | 16 | self.request_signer = request_signer or RequestSigner() 17 | 18 | @classmethod 19 | def from_pem_signing_key(cls, pem_string: str): 20 | request_signer = RequestSigner.from_pem(pem_string) 21 | return cls(request_signer) 22 | 23 | def _prepare_signed_request(self, request: httpx.Request) -> httpx.Request: 24 | path_and_query = request.url.raw_path.decode() 25 | authorization = request.headers.get("Authorization", "") 26 | 27 | body = b"" 28 | for byte in request.stream: 29 | body += byte 30 | 31 | signature = self.request_signer.sign( 32 | method=request.method, 33 | path_and_query=path_and_query, 34 | body=body, 35 | authorization=authorization, 36 | ) 37 | 38 | request.headers["Signature"] = signature 39 | return request 40 | 41 | async def send_request_signed(self, request: httpx.Request) -> httpx.Response: 42 | """ 43 | Shorthand for prepare signed + send 44 | """ 45 | prepared = self._prepare_signed_request(request) 46 | return await self.send(prepared) 47 | 48 | async def send_signed(self, method: str, url: str, **kwargs): 49 | """ 50 | Shorthand for creating request + prepare signed + send 51 | """ 52 | request = httpx.Request(method, url, **kwargs) 53 | prepared = self._prepare_signed_request(request) 54 | return await self.send(prepared) 55 | -------------------------------------------------------------------------------- /xbox/webapi/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from appdirs import user_data_dir 4 | 5 | CLIENT_ID = "388ea51c-0b25-4029-aae2-17df49d23905" 6 | # No secret needed, we registered as "Desktop App" in Azure AD 7 | CLIENT_SECRET = "" 8 | REDIRECT_URI = "http://localhost:8080/auth/callback" 9 | 10 | DATA_DIR = user_data_dir("xbox", "OpenXbox") 11 | TOKENS_FILE = os.path.join(DATA_DIR, "tokens.json") 12 | XAL_TOKENS_FILE = os.path.join(DATA_DIR, "xal_tokens.json") 13 | 14 | if not os.path.exists(DATA_DIR): 15 | os.makedirs(DATA_DIR) 16 | -------------------------------------------------------------------------------- /xbox/webapi/scripts/change_gamertag.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script that enables using your one-time-free gamertag change 3 | """ 4 | import argparse 5 | import asyncio 6 | import os 7 | import sys 8 | 9 | from httpx import HTTPStatusError 10 | 11 | from xbox.webapi.api.client import XboxLiveClient 12 | from xbox.webapi.api.provider.account.models import ( 13 | ChangeGamertagResult, 14 | ClaimGamertagResult, 15 | ) 16 | from xbox.webapi.authentication.manager import AuthenticationManager 17 | from xbox.webapi.authentication.models import OAuth2TokenResponse 18 | from xbox.webapi.common.signed_session import SignedSession 19 | from xbox.webapi.scripts import CLIENT_ID, CLIENT_SECRET, TOKENS_FILE 20 | 21 | 22 | async def async_main(): 23 | parser = argparse.ArgumentParser(description="Change your gamertag") 24 | parser.add_argument( 25 | "--tokens", 26 | "-t", 27 | default=TOKENS_FILE, 28 | help=f"Token filepath. Default: '{TOKENS_FILE}'", 29 | ) 30 | parser.add_argument( 31 | "--client-id", 32 | "-cid", 33 | default=os.environ.get("CLIENT_ID", CLIENT_ID), 34 | help="OAuth2 Client ID", 35 | ) 36 | parser.add_argument( 37 | "--client-secret", 38 | "-cs", 39 | default=os.environ.get("CLIENT_SECRET", CLIENT_SECRET), 40 | help="OAuth2 Client Secret", 41 | ) 42 | parser.add_argument("gamertag", help="Desired Gamertag") 43 | 44 | args = parser.parse_args() 45 | 46 | if len(args.gamertag) > 15: 47 | print("Desired gamertag exceedes limit of 15 chars") 48 | sys.exit(-1) 49 | 50 | if not os.path.exists(args.tokens): 51 | print("No token file found, run xbox-authenticate") 52 | sys.exit(-1) 53 | 54 | async with SignedSession() as session: 55 | auth_mgr = AuthenticationManager( 56 | session, args.client_id, args.client_secret, "" 57 | ) 58 | 59 | with open(args.tokens) as f: 60 | tokens = f.read() 61 | auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens) 62 | try: 63 | await auth_mgr.refresh_tokens() 64 | except HTTPStatusError: 65 | print("Could not refresh tokens") 66 | sys.exit(-1) 67 | 68 | with open(args.tokens, mode="w") as f: 69 | f.write(auth_mgr.oauth.json()) 70 | 71 | xbl_client = XboxLiveClient(auth_mgr) 72 | 73 | print( 74 | ":: Trying to change gamertag to '%s' for xuid '%i'..." 75 | % (args.gamertag, xbl_client.xuid) 76 | ) 77 | 78 | print("Claiming gamertag...") 79 | try: 80 | resp = await xbl_client.account.claim_gamertag( 81 | xbl_client.xuid, args.gamertag 82 | ) 83 | if resp == ClaimGamertagResult.NotAvailable: 84 | print("Claiming gamertag failed - Desired gamertag is unavailable") 85 | sys.exit(-1) 86 | except HTTPStatusError: 87 | print("Invalid HTTP response from claim") 88 | sys.exit(-1) 89 | 90 | print("Changing gamertag...") 91 | try: 92 | resp = await xbl_client.account.change_gamertag( 93 | xbl_client.xuid, args.gamertag 94 | ) 95 | if resp == ChangeGamertagResult.NoFreeChangesAvailable: 96 | print("Changing gamertag failed - You are out of free changes") 97 | sys.exit(-1) 98 | except HTTPStatusError: 99 | print("Invalid HTTP response from change") 100 | sys.exit(-1) 101 | 102 | print("Gamertag successfully changed to %s" % args.gamertag) 103 | 104 | 105 | def main(): 106 | asyncio.run(async_main()) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /xbox/webapi/scripts/friends.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script that enables using your one-time-free gamertag change 3 | """ 4 | import argparse 5 | import asyncio 6 | import os 7 | from pprint import pprint 8 | import sys 9 | 10 | from httpx import HTTPStatusError 11 | 12 | from xbox.webapi.api.client import XboxLiveClient 13 | from xbox.webapi.authentication.manager import AuthenticationManager 14 | from xbox.webapi.authentication.models import OAuth2TokenResponse 15 | from xbox.webapi.common.signed_session import SignedSession 16 | from xbox.webapi.scripts import CLIENT_ID, CLIENT_SECRET, TOKENS_FILE 17 | 18 | 19 | async def async_main(): 20 | parser = argparse.ArgumentParser(description="Change your gamertag") 21 | parser.add_argument( 22 | "--tokens", 23 | "-t", 24 | default=TOKENS_FILE, 25 | help=f"Token filepath. Default: '{TOKENS_FILE}'", 26 | ) 27 | parser.add_argument( 28 | "--client-id", 29 | "-cid", 30 | default=os.environ.get("CLIENT_ID", CLIENT_ID), 31 | help="OAuth2 Client ID", 32 | ) 33 | parser.add_argument( 34 | "--client-secret", 35 | "-cs", 36 | default=os.environ.get("CLIENT_SECRET", CLIENT_SECRET), 37 | help="OAuth2 Client Secret", 38 | ) 39 | 40 | args = parser.parse_args() 41 | 42 | if not os.path.exists(args.tokens): 43 | print("No token file found, run xbox-authenticate") 44 | sys.exit(-1) 45 | 46 | async with SignedSession() as session: 47 | auth_mgr = AuthenticationManager( 48 | session, args.client_id, args.client_secret, "" 49 | ) 50 | 51 | with open(args.tokens) as f: 52 | tokens = f.read() 53 | auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens) 54 | try: 55 | await auth_mgr.refresh_tokens() 56 | except HTTPStatusError: 57 | print("Could not refresh tokens") 58 | sys.exit(-1) 59 | 60 | with open(args.tokens, mode="w") as f: 61 | f.write(auth_mgr.oauth.json()) 62 | 63 | xbl_client = XboxLiveClient(auth_mgr) 64 | 65 | try: 66 | resp = await xbl_client.people.get_friends_own() 67 | except HTTPStatusError: 68 | print("Invalid HTTP response") 69 | sys.exit(-1) 70 | 71 | pprint(resp.dict()) 72 | 73 | 74 | def main(): 75 | asyncio.run(async_main()) 76 | 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /xbox/webapi/scripts/search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script that utilizes EDSProvider to search XBL marketplace 3 | """ 4 | import argparse 5 | import asyncio 6 | from pprint import pprint 7 | import sys 8 | 9 | from httpx import HTTPStatusError 10 | 11 | from xbox.webapi.api.client import XboxLiveClient 12 | from xbox.webapi.authentication.manager import AuthenticationManager 13 | from xbox.webapi.common.signed_session import SignedSession 14 | 15 | 16 | async def async_main(): 17 | parser = argparse.ArgumentParser(description="Search for Content on XBL") 18 | parser.add_argument("search_query", help="Name to search for") 19 | 20 | args = parser.parse_args() 21 | 22 | async with SignedSession() as session: 23 | auth_mgr = AuthenticationManager(session, "", "", "") 24 | 25 | # No Auth necessary for catalog searches 26 | xbl_client = XboxLiveClient(auth_mgr) 27 | 28 | try: 29 | resp = await xbl_client.catalog.product_search(args.search_query) 30 | except HTTPStatusError: 31 | print("Search failed") 32 | sys.exit(-1) 33 | 34 | pprint(resp.dict()) 35 | 36 | 37 | def main(): 38 | asyncio.run(async_main()) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /xbox/webapi/scripts/xal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example scripts that performs XBL authentication via XAL 3 | """ 4 | import argparse 5 | import asyncio 6 | import json 7 | import os 8 | import uuid 9 | 10 | from pydantic import BaseModel 11 | from pydantic.json import pydantic_encoder 12 | 13 | from xbox.webapi.authentication.models import ( 14 | SisuAuthorizationResponse, 15 | XalAppParameters, 16 | XalClientParameters, 17 | ) 18 | from xbox.webapi.authentication.xal import ( 19 | APP_PARAMS_GAMEPASS_BETA, 20 | CLIENT_PARAMS_ANDROID, 21 | XALManager, 22 | ) 23 | from xbox.webapi.common.signed_session import SignedSession 24 | from xbox.webapi.scripts import XAL_TOKENS_FILE 25 | 26 | 27 | class XALStore(BaseModel): 28 | """Used to store/load authorization data""" 29 | 30 | sisu: SisuAuthorizationResponse 31 | device_id: uuid.UUID 32 | app_params: XalAppParameters 33 | client_params: XalClientParameters 34 | 35 | 36 | def user_prompt_authentication(auth_url: str) -> str: 37 | """ 38 | Handles the auth callback when user is prompted to authenticate via URL 39 | in webbrowser 40 | 41 | Takes the redirect URL from stdin 42 | """ 43 | 44 | redirect_url = input( 45 | f"Continue auth with the following URL:\n\n" 46 | f"URL: {auth_url}\n\n" 47 | f"Provide redirect URI: " 48 | ) 49 | return redirect_url 50 | 51 | 52 | async def do_auth(device_id: uuid.UUID, token_filepath: str): 53 | async with SignedSession() as session: 54 | app_params = APP_PARAMS_GAMEPASS_BETA 55 | client_params = CLIENT_PARAMS_ANDROID 56 | 57 | store = None 58 | # Load existing sisu authorization data, if it exists 59 | if os.path.exists(token_filepath): 60 | with open(token_filepath) as f: 61 | store = json.load(f) 62 | 63 | # Convert SISU authorization data 64 | store = XALStore(**store) 65 | 66 | if store: 67 | raise NotImplementedError("Token refreshing") 68 | 69 | # Do authentication 70 | xal = XALManager(session, device_id, app_params, client_params) 71 | response = await xal.auth_flow(user_prompt_authentication) 72 | print(f"Sisu auth finished:\n\n{response}") 73 | 74 | # Save authorization data 75 | store = XALStore( 76 | sisu=response, 77 | device_id=device_id, 78 | app_params=app_params, 79 | client_params=client_params, 80 | ) 81 | 82 | with open(token_filepath, mode="w") as f: 83 | print(f"Finished authentication, writing tokens to {token_filepath}") 84 | json.dump(store, f, default=pydantic_encoder) 85 | 86 | 87 | async def async_main(): 88 | parser = argparse.ArgumentParser(description="Authenticate with XBL via XAL") 89 | parser.add_argument( 90 | "--tokens", 91 | "-t", 92 | default=XAL_TOKENS_FILE, 93 | help=f"Token filepath. Default: '{XAL_TOKENS_FILE}'", 94 | ) 95 | parser.add_argument( 96 | "--device-id", 97 | "-did", 98 | default=uuid.uuid4(), 99 | type=uuid.UUID, 100 | help="Device ID (for device auth)", 101 | ) 102 | args = parser.parse_args() 103 | 104 | await do_auth(args.device_id, args.tokens) 105 | 106 | 107 | def main(): 108 | asyncio.run(async_main()) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | --------------------------------------------------------------------------------