aiobungie.rest

Basic implementation of a RESTful clients for Bungie's REST API.

   1# MIT License
   2#
   3# Copyright (c) 2020 = Present nxtlo
   4#
   5# Permission is hereby granted, free of charge, to typing.Any person obtaining a copy
   6# of this software and associated documentation files (the "Software"), to deal
   7# in the Software without restriction, including without limitation the rights
   8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   9# copies of the Software, and to permit persons to whom the Software is
  10# furnished to do so, subject to the following conditions:
  11#
  12# The above copyright notice and this permission notice shall be included in all
  13# copies or substantial portions of the Software.
  14#
  15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF typing.Any KIND, EXPRESS OR
  16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR typing.Any CLAIM, DAMAGES OR OTHER
  19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21# SOFTWARE.
  22
  23"""Basic implementation of a RESTful clients for Bungie's REST API."""
  24
  25from __future__ import annotations
  26
  27__all__ = ("RESTClient", "RESTPool", "TRACE")
  28
  29import asyncio
  30import contextlib
  31import datetime
  32import http
  33import logging
  34import logging.config
  35import os
  36import pathlib
  37import sys
  38import typing
  39import uuid
  40import zipfile
  41
  42import aiohttp
  43
  44from aiobungie import api, builders, error, metadata, typedefs, url
  45from aiobungie.crates import clans, fireteams
  46from aiobungie.internal import _backoff as backoff
  47from aiobungie.internal import enums, helpers, time
  48
  49if typing.TYPE_CHECKING:
  50    import collections.abc as collections
  51    import concurrent.futures
  52    import types
  53
  54    _HTTP_METHOD = typing.Literal["GET", "DELETE", "POST", "PUT", "PATCH"]
  55    _ALLOWED_LANGS = typing.Literal[
  56        "en",
  57        "fr",
  58        "es",
  59        "es-mx",
  60        "de",
  61        "it",
  62        "ja",
  63        "pt-br",
  64        "ru",
  65        "pl",
  66        "ko",
  67        "zh-cht",
  68        "zh-chs",
  69    ]
  70
  71_MANIFEST_LANGUAGES: typing.Final[frozenset[_ALLOWED_LANGS]] = frozenset(
  72    (
  73        "en",
  74        "fr",
  75        "es",
  76        "es-mx",
  77        "de",
  78        "it",
  79        "ja",
  80        "pt-br",
  81        "ru",
  82        "pl",
  83        "ko",
  84        "zh-cht",
  85        "zh-chs",
  86    )
  87)
  88
  89# Client headers.
  90_APP_JSON: str = "application/json"
  91_AUTH_HEADER: str = sys.intern("Authorization")
  92_USER_AGENT_HEADERS: str = sys.intern("User-Agent")
  93_USER_AGENT: str = (
  94    f"AiobungieClient (Author: {metadata.__author__}), "
  95    f"(Version: {metadata.__version__}), (URL: {metadata.__url__})"
  96)
  97
  98# Possible internal error codes.
  99_RETRY_5XX: set[int] = {500, 502, 503, 504}
 100
 101# HTTP methods.
 102_GET: typing.Final[str] = "GET"
 103_POST: typing.Final[str] = "POST"
 104
 105# These are Currently unused.
 106# _DELETE: typing.Final[str] = "DELETE"
 107# _PUT: typing.Final[str] = "PUT"
 108# _PATCH: typing.Final[str] = "PATCH"
 109
 110
 111_LOGGER = logging.getLogger("aiobungie.rest")
 112
 113TRACE: typing.Final[int] = logging.DEBUG - 5
 114"""The trace logging level for the `RESTClient` responses.
 115
 116You can enable this with the following code
 117
 118>>> import logging
 119>>> logging.getLogger("aiobungie.rest").setLevel(aiobungie.TRACE)
 120# or
 121>>> logging.basicConfig(level=aiobungie.TRACE)
 122# Or
 123>>> client = aiobungie.RESTClient(debug="TRACE")
 124# Or if you're using `aiobungie.Client`
 125>>> client = aiobungie.Client()
 126>>> client.rest.with_debug(level=aiobungie.TRACE, file="rest_logs.txt")
 127"""
 128
 129logging.addLevelName(TRACE, "TRACE")
 130
 131
 132def _collect_components(
 133    components: collections.Sequence[enums.ComponentType],
 134    /,
 135) -> str:
 136    collector: collections.MutableSequence[str] = []
 137
 138    for component in components:
 139        if isinstance(component.value, tuple):
 140            collector.extend(str(c) for c in component.value)  # pyright: ignore
 141        else:
 142            collector.append(str(component.value))
 143    return ",".join(collector)
 144
 145
 146def _uuid() -> str:
 147    return uuid.uuid4().hex
 148
 149
 150def _ensure_manifest_language(language: str) -> None:
 151    if language not in _MANIFEST_LANGUAGES:
 152        langs = "\n".join(_MANIFEST_LANGUAGES)
 153        raise ValueError(
 154            f"{language} is not a valid manifest language, "
 155            f"valid languages are: {langs}"
 156        )
 157
 158
 159def _get_path(
 160    file_name: str, path: str | pathlib.Path, sql: bool = False
 161) -> pathlib.Path:
 162    if sql:
 163        return pathlib.Path(path).joinpath(file_name + ".sqlite3")
 164    return pathlib.Path(path).joinpath(file_name + ".json")
 165
 166
 167def _write_json_bytes(
 168    data: bytes,
 169    file_name: str = "manifest",
 170    path: pathlib.Path | str = "./",
 171) -> None:
 172    with _get_path(file_name, path).open("wb") as p:
 173        p.write(helpers.dumps(helpers.loads(data)))
 174
 175
 176def _write_sqlite_bytes(
 177    data: bytes,
 178    path: pathlib.Path | str = "./",
 179    file_name: str = "manifest",
 180) -> None:
 181    with open(f"{_uuid()}.zip", "wb") as tmp:
 182        tmp.write(data)
 183        try:
 184            with zipfile.ZipFile(tmp.name) as zipped:
 185                file = zipped.namelist()
 186
 187                if file:
 188                    zipped.extractall(".")
 189
 190                    os.rename(file[0], _get_path(file_name, path, sql=True))
 191
 192        finally:
 193            pathlib.Path(tmp.name).unlink(missing_ok=True)
 194
 195
 196class _JSONPayload(aiohttp.BytesPayload):
 197    def __init__(
 198        self, value: typing.Any, dumps: typedefs.Dumps = helpers.dumps
 199    ) -> None:
 200        super().__init__(dumps(value), content_type=_APP_JSON, encoding="utf-8")
 201
 202
 203class RESTPool:
 204    """a Pool of `RESTClient` instances that shares the same TCP client connection.
 205
 206    This allows you to acquire instances of `RESTClient`s from single settings and credentials.
 207
 208    Example
 209    -------
 210    ```py
 211    import aiobungie
 212    import asyncio
 213
 214    pool = aiobungie.RESTPool("token")
 215
 216    async def get() -> None:
 217        await pool.start()
 218
 219        async with pool.acquire() as client:
 220            await client.fetch_character(...)
 221
 222        await pool.stop()
 223
 224    asyncio.run(get())
 225    ```
 226
 227    Parameters
 228    ----------
 229    token : `str`
 230        A valid application token from Bungie's developer portal.
 231
 232    Other Parameters
 233    ----------------
 234    client_secret : `str | None`
 235        An optional application client secret,
 236        This is only needed if you're fetching OAuth2 tokens with this client.
 237    client_id : `int | None`
 238        An optional application client id,
 239        This is only needed if you're fetching OAuth2 tokens with this client.
 240    settings: `aiobungie.builders.Settings | None`
 241        The client settings to use, if `None` the default will be used.
 242    max_retries : `int`
 243        The max retries number to retry if the request hit a `5xx` status code.
 244    debug : `bool | str`
 245        Whether to enable logging responses or not.
 246
 247    Logging Levels
 248    --------------
 249    * `False`: This will disable logging.
 250    * `True`: This will set the level to `DEBUG` and enable logging minimal information.
 251    Like the response status, route, taken time and so on.
 252    * `"TRACE" | aiobungie.TRACE`: This will log the response headers along with the minimal information.
 253    """
 254
 255    __slots__ = (
 256        "_token",
 257        "_max_retries",
 258        "_client_secret",
 259        "_client_id",
 260        "_metadata",
 261        "_enable_debug",
 262        "_client_session",
 263        "_loads",
 264        "_dumps",
 265        "_settings",
 266    )
 267
 268    # Looks like mypy doesn't like this.
 269    if typing.TYPE_CHECKING:
 270        _enable_debug: typing.Literal["TRACE"] | bool | int
 271
 272    def __init__(
 273        self,
 274        token: str,
 275        /,
 276        *,
 277        client_secret: str | None = None,
 278        client_id: int | None = None,
 279        settings: builders.Settings | None = None,
 280        dumps: typedefs.Dumps = helpers.dumps,
 281        loads: typedefs.Loads = helpers.loads,
 282        max_retries: int = 4,
 283        debug: typing.Literal["TRACE"] | bool | int = False,
 284    ) -> None:
 285        self._client_secret = client_secret
 286        self._client_id = client_id
 287        self._token = token
 288        self._max_retries = max_retries
 289        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
 290        self._enable_debug = debug
 291        self._client_session: aiohttp.ClientSession | None = None
 292        self._loads = loads
 293        self._dumps = dumps
 294        self._settings = settings or builders.Settings()
 295
 296    @property
 297    def client_id(self) -> int | None:
 298        """Return the client id of this REST client if provided, Otherwise None."""
 299        return self._client_id
 300
 301    @property
 302    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
 303        """A general-purpose mutable mapping you can use to store data.
 304
 305        This mapping can be accessed from any process that has a reference to this pool.
 306        """
 307        return self._metadata
 308
 309    @property
 310    def settings(self) -> builders.Settings:
 311        """Internal client settings used within the HTTP client session."""
 312        return self._settings
 313
 314    @typing.overload
 315    def build_oauth2_url(self, client_id: int) -> builders.OAuthURL: ...
 316
 317    @typing.overload
 318    def build_oauth2_url(self) -> builders.OAuthURL | None: ...
 319
 320    @typing.final
 321    def build_oauth2_url(
 322        self, client_id: int | None = None
 323    ) -> builders.OAuthURL | None:
 324        """Construct a new `OAuthURL` url object.
 325
 326        You can get the complete string representation of the url by calling `.compile()` on it.
 327
 328        Parameters
 329        ----------
 330        client_id : `int | None`
 331            An optional client id to provide, If left `None` it will roll back to the id passed
 332            to the `RESTClient`, If both is `None` this method will return `None`.
 333
 334        Returns
 335        -------
 336        `aiobungie.builders.OAuthURL | None`
 337            * If `client_id` was provided as a parameter, It guarantees to return a complete `OAuthURL` object
 338            * If `client_id` is set to `aiobungie.RESTClient` will be.
 339            * If both are `None` this method will return `None.
 340        """
 341        client_id = client_id or self._client_id
 342        if client_id is None:
 343            return None
 344
 345        return builders.OAuthURL(client_id=client_id)
 346
 347    async def start(self) -> None:
 348        """Start the TCP connection of this client pool.
 349
 350        This will raise `RuntimeError` if the connection has already been started.
 351
 352        Example
 353        -------
 354        ```py
 355        pool = aiobungie.RESTPool(...)
 356
 357        async def run() -> None:
 358            await pool.start()
 359            async with pool.acquire() as client:
 360                # use client
 361
 362        async def stop(self) -> None:
 363            await pool.close()
 364        ```
 365        """
 366        if self._client_session is not None:
 367            raise RuntimeError("<RESTPool> has already been started.") from None
 368
 369        self._client_session = aiohttp.ClientSession(
 370            connector=aiohttp.TCPConnector(
 371                use_dns_cache=self._settings.use_dns_cache,
 372                ttl_dns_cache=self._settings.ttl_dns_cache,
 373                ssl_context=self._settings.ssl_context,
 374                ssl=self._settings.ssl,
 375            ),
 376            connector_owner=True,
 377            raise_for_status=False,
 378            timeout=self._settings.http_timeout,
 379            trust_env=self._settings.trust_env,
 380            headers=self._settings.headers,
 381        )
 382
 383    async def stop(self) -> None:
 384        """Stop the TCP connection of this client pool.
 385
 386        This will raise `RuntimeError` if the connection has already been closed.
 387
 388        Example
 389        -------
 390        ```py
 391        pool = aiobungie.RESTPool(...)
 392
 393        async def run() -> None:
 394            await pool.start()
 395            async with pool.acquire() as client:
 396                # use client
 397
 398        async def stop(self) -> None:
 399            await pool.close()
 400        ```
 401        """
 402        if self._client_session is None:
 403            raise RuntimeError("<RESTPool> is already stopped.")
 404
 405        await self._client_session.close()
 406        self._client_session = None
 407
 408    @typing.final
 409    def acquire(self) -> RESTClient:
 410        """Acquires a new `RESTClient` instance from this pool.
 411
 412        Returns
 413        -------
 414        `RESTClient`
 415            An instance of a `RESTClient`.
 416        """
 417        return RESTClient(
 418            self._token,
 419            client_secret=self._client_secret,
 420            client_id=self._client_id,
 421            loads=self._loads,
 422            dumps=self._dumps,
 423            max_retries=self._max_retries,
 424            debug=self._enable_debug,
 425            client_session=self._client_session,
 426            owned_client=False,
 427            settings=self._settings,
 428        )
 429
 430
 431class RESTClient(api.RESTClient):
 432    """A single process REST client implementation.
 433
 434    This client is designed to only make HTTP requests and return raw JSON objects.
 435
 436    Example
 437    -------
 438    ```py
 439    import aiobungie
 440
 441    client = aiobungie.RESTClient("TOKEN")
 442    async with client:
 443        response = await client.fetch_clan_members(4389205)
 444        for member in response['results']:
 445            print(member['destinyUserInfo'])
 446    ```
 447
 448    Parameters
 449    ----------
 450    token : `str`
 451        A valid application token from Bungie's developer portal.
 452
 453    Other Parameters
 454    ----------------
 455    client_secret : `str | None`
 456        An optional application client secret,
 457        This is only needed if you're fetching OAuth2 tokens with this client.
 458    client_id : `int | None`
 459        An optional application client id,
 460        This is only needed if you're fetching OAuth2 tokens with this client.
 461    settings: `aiobungie.builders.Settings | None`
 462        The client settings to use, if `None` the default will be used.
 463    owned_client: `bool`
 464        * If set to `True`, this client will use the provided `client_session` parameter instead,
 465        * If set to `True` and `client_session` is `None`, `ValueError` will be raised.
 466        * If set to `False`, aiobungie will initialize a new client session for you.
 467
 468    client_session: `aiohttp.ClientSession | None`
 469        If provided, this client session will be used to make all the HTTP requests.
 470        The `owned_client` must be set to `True` for this to work.
 471    max_retries : `int`
 472        The max retries number to retry if the request hit a `5xx` status code.
 473    debug : `bool | str`
 474        Whether to enable logging responses or not.
 475
 476    Logging Levels
 477    --------------
 478    * `False`: This will disable logging.
 479    * `True`: This will set the level to `DEBUG` and enable logging minimal information.
 480    * `"TRACE" | aiobungie.TRACE`: This will log the response headers along with the minimal information.
 481    """
 482
 483    __slots__ = (
 484        "_token",
 485        "_session",
 486        "_lock",
 487        "_max_retries",
 488        "_client_secret",
 489        "_client_id",
 490        "_metadata",
 491        "_dumps",
 492        "_loads",
 493        "_owned_client",
 494        "_settings",
 495    )
 496
 497    def __init__(
 498        self,
 499        token: str,
 500        /,
 501        *,
 502        client_secret: str | None = None,
 503        client_id: int | None = None,
 504        settings: builders.Settings | None = None,
 505        owned_client: bool = True,
 506        client_session: aiohttp.ClientSession | None = None,
 507        dumps: typedefs.Dumps = helpers.dumps,
 508        loads: typedefs.Loads = helpers.loads,
 509        max_retries: int = 4,
 510        debug: typing.Literal["TRACE"] | bool | int = False,
 511    ) -> None:
 512        if owned_client is False and client_session is None:
 513            raise ValueError(
 514                "Expected an owned client session, but got `None`, Cannot have `owned_client` set to `False` and `client_session` to `None`"
 515            )
 516
 517        self._settings = settings or builders.Settings()
 518        self._session = client_session
 519        self._owned_client = owned_client
 520        self._lock: asyncio.Lock | None = None
 521        self._client_secret = client_secret
 522        self._client_id = client_id
 523        self._token: str = token
 524        self._max_retries = max_retries
 525        self._dumps = dumps
 526        self._loads = loads
 527        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
 528        self.with_debug(debug)
 529
 530    @property
 531    def client_id(self) -> int | None:
 532        return self._client_id
 533
 534    @property
 535    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
 536        return self._metadata
 537
 538    @property
 539    def is_alive(self) -> bool:
 540        return self._session is not None
 541
 542    @property
 543    def settings(self) -> builders.Settings:
 544        return self._settings
 545
 546    async def close(self) -> None:
 547        if self._session is None:
 548            raise RuntimeError("REST client is not running.")
 549
 550        if self._owned_client:
 551            await self._session.close()
 552            self._session = None
 553
 554    def open(self) -> None:
 555        """Open a new client session. This is called internally with contextmanager usage."""
 556        if self.is_alive and self._owned_client:
 557            raise RuntimeError("Cannot open REST client when it's already open.")
 558
 559        if self._owned_client:
 560            self._session = aiohttp.ClientSession(
 561                connector=aiohttp.TCPConnector(
 562                    use_dns_cache=self._settings.use_dns_cache,
 563                    ttl_dns_cache=self._settings.ttl_dns_cache,
 564                    ssl_context=self._settings.ssl_context,
 565                    ssl=self._settings.ssl,
 566                ),
 567                connector_owner=True,
 568                raise_for_status=False,
 569                timeout=self._settings.http_timeout,
 570                trust_env=self._settings.trust_env,
 571                headers=self._settings.headers,
 572            )
 573
 574    @typing.final
 575    async def static_request(
 576        self,
 577        method: _HTTP_METHOD,
 578        path: str,
 579        *,
 580        auth: str | None = None,
 581        json: collections.Mapping[str, typing.Any] | None = None,
 582        params: collections.Mapping[str, typing.Any] | None = None,
 583    ) -> typedefs.JSONIsh:
 584        return await self._request(method, path, auth=auth, json=json, params=params)
 585
 586    @typing.overload
 587    def build_oauth2_url(self, client_id: int) -> builders.OAuthURL: ...
 588
 589    @typing.overload
 590    def build_oauth2_url(self) -> builders.OAuthURL | None: ...
 591
 592    @typing.final
 593    def build_oauth2_url(
 594        self, client_id: int | None = None
 595    ) -> builders.OAuthURL | None:
 596        client_id = client_id or self._client_id
 597        if client_id is None:
 598            return None
 599
 600        return builders.OAuthURL(client_id=client_id)
 601
 602    @typing.final
 603    async def _request(
 604        self,
 605        method: _HTTP_METHOD,
 606        route: str,
 607        *,
 608        base: bool = False,
 609        oauth2: bool = False,
 610        auth: str | None = None,
 611        unwrap_bytes: bool = False,
 612        json: collections.Mapping[str, typing.Any] | None = None,
 613        data: collections.Mapping[str, typing.Any] | None = None,
 614        params: collections.Mapping[str, typing.Any] | None = None,
 615    ) -> typedefs.JSONIsh:
 616        # This is not None when opening the client.
 617        assert self._session is not None, (
 618            "This client hasn't been opened yet. Use `async with client` or `async with client.rest` "
 619            "before performing any request."
 620        )
 621
 622        retries: int = 0
 623        headers: collections.MutableMapping[str, typing.Any] = {}
 624
 625        headers[_USER_AGENT_HEADERS] = _USER_AGENT
 626        headers["X-API-KEY"] = self._token
 627
 628        if auth is not None:
 629            headers[_AUTH_HEADER] = f"Bearer {auth}"
 630
 631        # Handling endpoints
 632        endpoint = url.BASE
 633
 634        if not base:
 635            endpoint = endpoint + url.REST_EP
 636
 637        if oauth2:
 638            assert self._client_id, "Client ID is required to make authorized requests."
 639            assert (
 640                self._client_secret
 641            ), "Client secret is required to make authorized requests."
 642            headers["client_secret"] = self._client_secret
 643
 644            headers["Content-Type"] = "application/x-www-form-urlencoded"
 645            endpoint = endpoint + url.TOKEN_EP
 646
 647        if self._lock is None:
 648            self._lock = asyncio.Lock()
 649
 650        if json:
 651            headers["Content-Type"] = _APP_JSON
 652
 653        stack = contextlib.AsyncExitStack()
 654        while True:
 655            try:
 656                await stack.enter_async_context(self._lock)
 657
 658                # We make the request here.
 659                taken_time = time.monotonic()
 660                response = await self._session.request(
 661                    method=method,
 662                    url=f"{endpoint}/{route}",
 663                    headers=headers,
 664                    data=_JSONPayload(json) if json else data,
 665                    params=params,
 666                )
 667                response_time = (time.monotonic() - taken_time) * 1_000
 668
 669                _LOGGER.debug(
 670                    "METHOD: %s ROUTE: %s STATUS: %i ELAPSED: %.4fms",
 671                    method,
 672                    f"{endpoint}/{route}",
 673                    response.status,
 674                    response_time,
 675                )
 676
 677                await self._handle_ratelimit(response, method, route)
 678
 679            except aiohttp.ClientConnectionError as exc:
 680                if retries >= self._max_retries:
 681                    raise error.HTTPError(
 682                        str(exc),
 683                        http.HTTPStatus.SERVICE_UNAVAILABLE,
 684                    )
 685                backoff_ = backoff.ExponentialBackOff(maximum=8)
 686
 687                timer = next(backoff_)
 688                _LOGGER.warning(
 689                    "Client received a connection error <%s> Retrying in %.2fs. Remaining retries: %s",
 690                    type(exc).__qualname__,
 691                    timer,
 692                    self._max_retries - retries,
 693                )
 694                retries += 1
 695                await asyncio.sleep(timer)
 696                continue
 697
 698            finally:
 699                await stack.aclose()
 700
 701            if response.status == http.HTTPStatus.NO_CONTENT:
 702                return None
 703
 704            # Handle the successful response.
 705            if 300 > response.status >= 200:
 706                if unwrap_bytes:
 707                    # We need to read the bytes for the manifest response.
 708                    return await response.read()
 709
 710                # Bungie get funky and return HTML instead of JSON when making an authorized
 711                # request with a dummy access token. We could technically read the page content
 712                # but that's Bungie's fault for not returning a JSON response.
 713                if response.content_type != _APP_JSON:
 714                    raise error.HTTPError(
 715                        message=f"Expected JSON response, Got {response.content_type}, "
 716                        f"{response.real_url.human_repr()}",
 717                        http_status=http.HTTPStatus(response.status),
 718                    )
 719
 720                json_data = self._loads(await response.read())
 721
 722                if _LOGGER.isEnabledFor(TRACE):
 723                    _LOGGER.log(
 724                        TRACE,
 725                        "%s",
 726                        error.stringify_headers(dict(response.headers)),
 727                    )
 728
 729                    details: collections.MutableMapping[str, typing.Any] = {}
 730                    if json:
 731                        details["json"] = error.filtered_headers(json)
 732
 733                    if data:
 734                        details["data"] = error.filtered_headers(data)
 735
 736                    if params:
 737                        details["params"] = error.filtered_headers(params)
 738
 739                    if details:
 740                        _LOGGER.log(TRACE, "%s", error.stringify_headers(details))
 741
 742                # Return the response.
 743                # auth responses are not inside a Response object.
 744                if oauth2:
 745                    return json_data
 746
 747                # The reason we have a type ignore is because the actual response type
 748                # is within this `Response` key.
 749                return json_data["Response"]  # type: ignore
 750
 751            if (
 752                response.status in _RETRY_5XX and retries < self._max_retries  # noqa: W503
 753            ):
 754                backoff_ = backoff.ExponentialBackOff(maximum=6)
 755                sleep_time = next(backoff_)
 756                _LOGGER.warning(
 757                    "Got %i - %s. Sleeping for %.2f seconds. Remaining retries: %i",
 758                    response.status,
 759                    response.reason,
 760                    sleep_time,
 761                    self._max_retries - retries,
 762                )
 763
 764                retries += 1
 765                await asyncio.sleep(sleep_time)
 766                continue
 767
 768            raise await error.panic(response)
 769
 770    async def __aenter__(self) -> RESTClient:
 771        self.open()
 772        return self
 773
 774    async def __aexit__(
 775        self,
 776        exception_type: type[BaseException] | None,
 777        exception: BaseException | None,
 778        exception_traceback: types.TracebackType | None,
 779    ) -> None:
 780        await self.close()
 781
 782    # We don't want this to be super complicated.
 783    async def _handle_ratelimit(
 784        self,
 785        response: aiohttp.ClientResponse,
 786        method: str,
 787        route: str,
 788    ) -> None:
 789        if response.status != http.HTTPStatus.TOO_MANY_REQUESTS:
 790            return
 791
 792        if response.content_type != _APP_JSON:
 793            raise error.HTTPError(
 794                f"Being ratelimited on non JSON request, {response.content_type}.",
 795                http.HTTPStatus.TOO_MANY_REQUESTS,
 796            )
 797
 798        # The reason we have a type ignore here is that we guaranteed the content type is JSON above.
 799        json: typedefs.JSONObject = self._loads(await response.read())  # type: ignore
 800        retry_after = float(json.get("ThrottleSeconds", 15.0)) + 0.1
 801        max_calls: int = 0
 802
 803        while True:
 804            if max_calls == 10:
 805                # Max retries by default. We raise an error here.
 806                raise error.RateLimitedError(
 807                    body=json,
 808                    url=str(response.real_url),
 809                    retry_after=retry_after,
 810                )
 811
 812            # We sleep for a little bit to avoid funky behavior.
 813            _LOGGER.warning(
 814                "We're being ratelimited, Method %s Route %s. Sleeping for %.2fs.",
 815                method,
 816                route,
 817                retry_after,
 818            )
 819            await asyncio.sleep(retry_after)
 820            max_calls += 1
 821            continue
 822
 823    async def fetch_oauth2_tokens(self, code: str, /) -> builders.OAuth2Response:
 824        data = {
 825            "grant_type": "authorization_code",
 826            "code": code,
 827            "client_id": self._client_id,
 828            "client_secret": self._client_secret,
 829        }
 830
 831        response = await self._request(_POST, "", data=data, oauth2=True)
 832        assert isinstance(response, dict)
 833        return builders.OAuth2Response.build_response(response)
 834
 835    async def refresh_access_token(
 836        self, refresh_token: str, /
 837    ) -> builders.OAuth2Response:
 838        data = {
 839            "grant_type": "refresh_token",
 840            "refresh_token": refresh_token,
 841            "client_id": self._client_id,
 842            "client_secret": self._client_secret,
 843        }
 844
 845        response = await self._request(_POST, "", data=data, oauth2=True)
 846        assert isinstance(response, dict)
 847        return builders.OAuth2Response.build_response(response)
 848
 849    async def fetch_bungie_user(self, id: int) -> typedefs.JSONObject:
 850        resp = await self._request(_GET, f"User/GetBungieNetUserById/{id}/")
 851        assert isinstance(resp, dict)
 852        return resp
 853
 854    async def fetch_user_themes(self) -> typedefs.JSONArray:
 855        resp = await self._request(_GET, "User/GetAvailableThemes/")
 856        assert isinstance(resp, list)
 857        return resp
 858
 859    async def fetch_membership_from_id(
 860        self,
 861        id: int,
 862        type: enums.MembershipType | int = enums.MembershipType.NONE,
 863        /,
 864    ) -> typedefs.JSONObject:
 865        resp = await self._request(_GET, f"User/GetMembershipsById/{id}/{int(type)}")
 866        assert isinstance(resp, dict)
 867        return resp
 868
 869    async def fetch_membership(
 870        self,
 871        name: str,
 872        code: int,
 873        type: enums.MembershipType | int = enums.MembershipType.ALL,
 874        /,
 875    ) -> typedefs.JSONArray:
 876        resp = await self._request(
 877            _POST,
 878            f"Destiny2/SearchDestinyPlayerByBungieName/{int(type)}",
 879            json={"displayName": name, "displayNameCode": code},
 880        )
 881        assert isinstance(resp, list)
 882        return resp
 883
 884    async def fetch_sanitized_membership(
 885        self, membership_id: int, /
 886    ) -> typedefs.JSONObject:
 887        response = await self._request(
 888            _GET, f"User/GetSanitizedPlatformDisplayNames/{membership_id}/"
 889        )
 890        assert isinstance(response, dict)
 891        return response
 892
 893    async def search_users(self, name: str, /) -> typedefs.JSONObject:
 894        resp = await self._request(
 895            _POST,
 896            "User/Search/GlobalName/0",
 897            json={"displayNamePrefix": name},
 898        )
 899        assert isinstance(resp, dict)
 900        return resp
 901
 902    async def fetch_clan_from_id(
 903        self, id: int, /, access_token: str | None = None
 904    ) -> typedefs.JSONObject:
 905        resp = await self._request(_GET, f"GroupV2/{id}", auth=access_token)
 906        assert isinstance(resp, dict)
 907        return resp
 908
 909    async def fetch_clan(
 910        self,
 911        name: str,
 912        /,
 913        access_token: str | None = None,
 914        *,
 915        type: enums.GroupType | int = enums.GroupType.CLAN,
 916    ) -> typedefs.JSONObject:
 917        resp = await self._request(
 918            _GET, f"GroupV2/Name/{name}/{int(type)}", auth=access_token
 919        )
 920        assert isinstance(resp, dict)
 921        return resp
 922
 923    async def search_group(
 924        self,
 925        name: str,
 926        group_type: enums.GroupType | int = enums.GroupType.CLAN,
 927        *,
 928        creation_date: clans.GroupDate | int = 0,
 929        sort_by: int | None = None,
 930        group_member_count_filter: typing.Literal[0, 1, 2, 3] | None = None,
 931        locale_filter: str | None = None,
 932        tag_text: str | None = None,
 933        items_per_page: int | None = None,
 934        current_page: int | None = None,
 935        request_token: str | None = None,
 936    ) -> typedefs.JSONObject:
 937        payload: collections.MutableMapping[str, typing.Any] = {"name": name}
 938
 939        # as the official documentation says, you're not allowed to use those fields
 940        # on a clan search. it is safe to send the request with them being `null` but not filled with a value.
 941        if (
 942            group_type == enums.GroupType.CLAN
 943            and group_member_count_filter is not None
 944            and locale_filter
 945            and tag_text
 946        ):
 947            raise ValueError(
 948                "If you're searching for clans, (group_member_count_filter, locale_filter, tag_text) must be None."
 949            )
 950
 951        payload["groupType"] = int(group_type)
 952        payload["creationDate"] = int(creation_date)
 953        payload["sortBy"] = sort_by
 954        payload["groupMemberCount"] = group_member_count_filter
 955        payload["locale"] = locale_filter
 956        payload["tagText"] = tag_text
 957        payload["itemsPerPage"] = items_per_page
 958        payload["currentPage"] = current_page
 959        payload["requestToken"] = request_token
 960        payload["requestContinuationToken"] = request_token
 961
 962        resp = await self._request(_POST, "GroupV2/Search/", json=payload)
 963        assert isinstance(resp, dict)
 964        return resp
 965
 966    async def fetch_clan_admins(self, clan_id: int, /) -> typedefs.JSONObject:
 967        resp = await self._request(_GET, f"GroupV2/{clan_id}/AdminsAndFounder/")
 968        assert isinstance(resp, dict)
 969        return resp
 970
 971    async def fetch_clan_conversations(self, clan_id: int, /) -> typedefs.JSONArray:
 972        resp = await self._request(_GET, f"GroupV2/{clan_id}/OptionalConversations/")
 973        assert isinstance(resp, list)
 974        return resp
 975
 976    async def fetch_application(self, appid: int, /) -> typedefs.JSONObject:
 977        resp = await self._request(_GET, f"App/Application/{appid}")
 978        assert isinstance(resp, dict)
 979        return resp
 980
 981    async def fetch_character(
 982        self,
 983        member_id: int,
 984        membership_type: enums.MembershipType | int,
 985        character_id: int,
 986        components: collections.Sequence[enums.ComponentType],
 987        auth: str | None = None,
 988    ) -> typedefs.JSONObject:
 989        collector = _collect_components(components)
 990        response = await self._request(
 991            _GET,
 992            f"Destiny2/{int(membership_type)}/Profile/{member_id}/"
 993            f"Character/{character_id}/?components={collector}",
 994            auth=auth,
 995        )
 996        assert isinstance(response, dict)
 997        return response
 998
 999    async def fetch_activities(
1000        self,
1001        member_id: int,
1002        character_id: int,
1003        mode: enums.GameMode | int,
1004        membership_type: enums.MembershipType | int = enums.MembershipType.ALL,
1005        *,
1006        page: int = 0,
1007        limit: int = 1,
1008    ) -> typedefs.JSONObject:
1009        resp = await self._request(
1010            _GET,
1011            f"Destiny2/{int(membership_type)}/Account/"
1012            f"{member_id}/Character/{character_id}/Stats/Activities"
1013            f"/?mode={int(mode)}&count={limit}&page={page}",
1014        )
1015        assert isinstance(resp, dict)
1016        return resp
1017
1018    async def fetch_vendor_sales(self) -> typedefs.JSONObject:
1019        resp = await self._request(
1020            _GET,
1021            f"Destiny2/Vendors/?components={int(enums.ComponentType.VENDOR_SALES)}",
1022        )
1023        assert isinstance(resp, dict)
1024        return resp
1025
1026    async def fetch_profile(
1027        self,
1028        membership_id: int,
1029        type: enums.MembershipType | int,
1030        components: collections.Sequence[enums.ComponentType],
1031        auth: str | None = None,
1032    ) -> typedefs.JSONObject:
1033        collector = _collect_components(components)
1034        response = await self._request(
1035            _GET,
1036            f"Destiny2/{int(type)}/Profile/{membership_id}/?components={collector}",
1037            auth=auth,
1038        )
1039        assert isinstance(response, dict)
1040        return response
1041
1042    async def fetch_entity(self, type: str, hash: int) -> typedefs.JSONObject:
1043        response = await self._request(_GET, route=f"Destiny2/Manifest/{type}/{hash}")
1044        assert isinstance(response, dict)
1045        return response
1046
1047    async def fetch_inventory_item(self, hash: int, /) -> typedefs.JSONObject:
1048        resp = await self.fetch_entity("DestinyInventoryItemDefinition", hash)
1049        assert isinstance(resp, dict)
1050        return resp
1051
1052    async def fetch_objective_entity(self, hash: int, /) -> typedefs.JSONObject:
1053        resp = await self.fetch_entity("DestinyObjectiveDefinition", hash)
1054        assert isinstance(resp, dict)
1055        return resp
1056
1057    async def fetch_groups_for_member(
1058        self,
1059        member_id: int,
1060        member_type: enums.MembershipType | int,
1061        /,
1062        *,
1063        filter: int = 0,
1064        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1065    ) -> typedefs.JSONObject:
1066        resp = await self._request(
1067            _GET,
1068            f"GroupV2/User/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1069        )
1070        assert isinstance(resp, dict)
1071        return resp
1072
1073    async def fetch_potential_groups_for_member(
1074        self,
1075        member_id: int,
1076        member_type: enums.MembershipType | int,
1077        /,
1078        *,
1079        filter: int = 0,
1080        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1081    ) -> typedefs.JSONObject:
1082        resp = await self._request(
1083            _GET,
1084            f"GroupV2/User/Potential/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1085        )
1086        assert isinstance(resp, dict)
1087        return resp
1088
1089    async def fetch_clan_members(
1090        self,
1091        clan_id: int,
1092        /,
1093        *,
1094        name: str | None = None,
1095        type: enums.MembershipType | int = enums.MembershipType.NONE,
1096    ) -> typedefs.JSONObject:
1097        resp = await self._request(
1098            _GET,
1099            f"/GroupV2/{clan_id}/Members/?memberType={int(type)}&nameSearch={name if name else ''}&currentpage=1",
1100        )
1101        assert isinstance(resp, dict)
1102        return resp
1103
1104    async def fetch_hardlinked_credentials(
1105        self,
1106        credential: int,
1107        type: enums.CredentialType | int = enums.CredentialType.STEAMID,
1108        /,
1109    ) -> typedefs.JSONObject:
1110        resp = await self._request(
1111            _GET,
1112            f"User/GetMembershipFromHardLinkedCredential/{int(type)}/{credential}/",
1113        )
1114        assert isinstance(resp, dict)
1115        return resp
1116
1117    async def fetch_user_credentials(
1118        self, access_token: str, membership_id: int, /
1119    ) -> typedefs.JSONArray:
1120        resp = await self._request(
1121            _GET,
1122            f"User/GetCredentialTypesForTargetAccount/{membership_id}",
1123            auth=access_token,
1124        )
1125        assert isinstance(resp, list)
1126        return resp
1127
1128    async def insert_socket_plug(
1129        self,
1130        action_token: str,
1131        /,
1132        instance_id: int,
1133        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1134        character_id: int,
1135        membership_type: enums.MembershipType | int,
1136    ) -> typedefs.JSONObject:
1137        if isinstance(plug, builders.PlugSocketBuilder):
1138            plug = plug.collect()
1139
1140        body = {
1141            "actionToken": action_token,
1142            "itemInstanceId": instance_id,
1143            "plug": plug,
1144            "characterId": character_id,
1145            "membershipType": int(membership_type),
1146        }
1147        resp = await self._request(
1148            _POST, "Destiny2/Actions/Items/InsertSocketPlug", json=body
1149        )
1150        assert isinstance(resp, dict)
1151        return resp
1152
1153    async def insert_socket_plug_free(
1154        self,
1155        access_token: str,
1156        /,
1157        instance_id: int,
1158        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1159        character_id: int,
1160        membership_type: enums.MembershipType | int,
1161    ) -> typedefs.JSONObject:
1162        if isinstance(plug, builders.PlugSocketBuilder):
1163            plug = plug.collect()
1164
1165        body = {
1166            "itemInstanceId": instance_id,
1167            "plug": plug,
1168            "characterId": character_id,
1169            "membershipType": int(membership_type),
1170        }
1171        resp = await self._request(
1172            _POST,
1173            "Destiny2/Actions/Items/InsertSocketPlugFree",
1174            json=body,
1175            auth=access_token,
1176        )
1177        assert isinstance(resp, dict)
1178        return resp
1179
1180    @helpers.unstable
1181    async def set_item_lock_state(
1182        self,
1183        access_token: str,
1184        state: bool,
1185        /,
1186        item_id: int,
1187        character_id: int,
1188        membership_type: enums.MembershipType | int,
1189    ) -> int:
1190        body = {
1191            "state": state,
1192            "itemId": item_id,
1193            "characterId": character_id,
1194            "membershipType": int(membership_type),
1195        }
1196        response = await self._request(
1197            _POST,
1198            "Destiny2/Actions/Items/SetLockState",
1199            json=body,
1200            auth=access_token,
1201        )
1202        assert isinstance(response, int)
1203        return response
1204
1205    async def set_quest_track_state(
1206        self,
1207        access_token: str,
1208        state: bool,
1209        /,
1210        item_id: int,
1211        character_id: int,
1212        membership_type: enums.MembershipType | int,
1213    ) -> int:
1214        body = {
1215            "state": state,
1216            "itemId": item_id,
1217            "characterId": character_id,
1218            "membership_type": int(membership_type),
1219        }
1220        response = await self._request(
1221            _POST,
1222            "Destiny2/Actions/Items/SetTrackedState",
1223            json=body,
1224            auth=access_token,
1225        )
1226        assert isinstance(response, int)
1227        return response
1228
1229    async def fetch_manifest_path(self) -> typedefs.JSONObject:
1230        path = await self._request(_GET, "Destiny2/Manifest")
1231        assert isinstance(path, dict)
1232        return path
1233
1234    async def read_manifest_bytes(self, language: _ALLOWED_LANGS = "en", /) -> bytes:
1235        _ensure_manifest_language(language)
1236
1237        content = await self.fetch_manifest_path()
1238        resp = await self._request(
1239            _GET,
1240            content["mobileWorldContentPaths"][language],
1241            unwrap_bytes=True,
1242            base=True,
1243        )
1244        assert isinstance(resp, bytes)
1245        return resp
1246
1247    async def download_sqlite_manifest(
1248        self,
1249        language: _ALLOWED_LANGS = "en",
1250        name: str = "manifest",
1251        path: pathlib.Path | str = ".",
1252        *,
1253        force: bool = False,
1254        executor: concurrent.futures.Executor | None = None,
1255    ) -> pathlib.Path:
1256        complete_path = _get_path(name, path, sql=True)
1257
1258        if complete_path.exists():
1259            if force:
1260                _LOGGER.info(
1261                    f"Found manifest in {complete_path!s}. Forcing to Re-Download."
1262                )
1263                complete_path.unlink(missing_ok=True)
1264
1265                return await self.download_sqlite_manifest(
1266                    language, name, path, force=force
1267                )
1268
1269            else:
1270                raise FileExistsError(
1271                    "Manifest file already exists, "
1272                    "To force download, set the `force` parameter to `True`."
1273                )
1274
1275        _LOGGER.info(f"Downloading manifest. Location: {complete_path!s}")
1276        data_bytes = await self.read_manifest_bytes(language)
1277        await asyncio.get_running_loop().run_in_executor(
1278            executor, _write_sqlite_bytes, data_bytes, path, name
1279        )
1280        _LOGGER.info("Finished downloading manifest.")
1281        return _get_path(name, path, sql=True)
1282
1283    async def download_json_manifest(
1284        self,
1285        file_name: str = "manifest",
1286        path: str | pathlib.Path = ".",
1287        *,
1288        language: _ALLOWED_LANGS = "en",
1289        executor: concurrent.futures.Executor | None = None,
1290    ) -> pathlib.Path:
1291        _ensure_manifest_language(language)
1292        full_path = _get_path(file_name, path)
1293        _LOGGER.info(f"Downloading manifest JSON to {full_path!r}...")
1294
1295        content = await self.fetch_manifest_path()
1296        json_bytes = await self._request(
1297            _GET,
1298            content["jsonWorldContentPaths"][language],
1299            unwrap_bytes=True,
1300            base=True,
1301        )
1302
1303        assert isinstance(json_bytes, bytes)
1304        await asyncio.get_running_loop().run_in_executor(
1305            executor, _write_json_bytes, json_bytes, file_name, path
1306        )
1307        _LOGGER.info("Finished downloading manifest JSON.")
1308        return full_path
1309
1310    async def fetch_manifest_version(self) -> str:
1311        # This is guaranteed str.
1312        return (await self.fetch_manifest_path())["version"]
1313
1314    async def fetch_linked_profiles(
1315        self,
1316        member_id: int,
1317        member_type: enums.MembershipType | int,
1318        /,
1319        *,
1320        all: bool = False,
1321    ) -> typedefs.JSONObject:
1322        resp = await self._request(
1323            _GET,
1324            f"Destiny2/{int(member_type)}/Profile/{member_id}/LinkedProfiles/?getAllMemberships={all}",
1325        )
1326        assert isinstance(resp, dict)
1327        return resp
1328
1329    async def fetch_clan_banners(self) -> typedefs.JSONObject:
1330        resp = await self._request(_GET, "Destiny2/Clan/ClanBannerDictionary/")
1331        assert isinstance(resp, dict)
1332        return resp
1333
1334    async def fetch_public_milestones(self) -> typedefs.JSONObject:
1335        resp = await self._request(_GET, "Destiny2/Milestones/")
1336        assert isinstance(resp, dict)
1337        return resp
1338
1339    async def fetch_public_milestone_content(
1340        self, milestone_hash: int, /
1341    ) -> typedefs.JSONObject:
1342        resp = await self._request(
1343            _GET, f"Destiny2/Milestones/{milestone_hash}/Content/"
1344        )
1345        assert isinstance(resp, dict)
1346        return resp
1347
1348    async def fetch_current_user_memberships(
1349        self, access_token: str, /
1350    ) -> typedefs.JSONObject:
1351        resp = await self._request(
1352            _GET,
1353            "User/GetMembershipsForCurrentUser/",
1354            auth=access_token,
1355        )
1356        assert isinstance(resp, dict)
1357        return resp
1358
1359    async def equip_item(
1360        self,
1361        access_token: str,
1362        /,
1363        item_id: int,
1364        character_id: int,
1365        membership_type: enums.MembershipType | int,
1366    ) -> None:
1367        payload = {
1368            "itemId": item_id,
1369            "characterId": character_id,
1370            "membershipType": int(membership_type),
1371        }
1372
1373        await self._request(
1374            _POST,
1375            "Destiny2/Actions/Items/EquipItem/",
1376            json=payload,
1377            auth=access_token,
1378        )
1379
1380    async def equip_items(
1381        self,
1382        access_token: str,
1383        /,
1384        item_ids: collections.Sequence[int],
1385        character_id: int,
1386        membership_type: enums.MembershipType | int,
1387    ) -> None:
1388        payload = {
1389            "itemIds": item_ids,
1390            "characterId": character_id,
1391            "membershipType": int(membership_type),
1392        }
1393        await self._request(
1394            _POST,
1395            "Destiny2/Actions/Items/EquipItems/",
1396            json=payload,
1397            auth=access_token,
1398        )
1399
1400    async def ban_clan_member(
1401        self,
1402        access_token: str,
1403        /,
1404        group_id: int,
1405        membership_id: int,
1406        membership_type: enums.MembershipType | int,
1407        *,
1408        length: int = 0,
1409        comment: str | None = None,
1410    ) -> None:
1411        payload = {"comment": str(comment), "length": length}
1412        await self._request(
1413            _POST,
1414            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Ban/",
1415            json=payload,
1416            auth=access_token,
1417        )
1418
1419    async def unban_clan_member(
1420        self,
1421        access_token: str,
1422        /,
1423        group_id: int,
1424        membership_id: int,
1425        membership_type: enums.MembershipType | int,
1426    ) -> None:
1427        await self._request(
1428            _POST,
1429            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Unban/",
1430            auth=access_token,
1431        )
1432
1433    async def kick_clan_member(
1434        self,
1435        access_token: str,
1436        /,
1437        group_id: int,
1438        membership_id: int,
1439        membership_type: enums.MembershipType | int,
1440    ) -> typedefs.JSONObject:
1441        resp = await self._request(
1442            _POST,
1443            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Kick/",
1444            auth=access_token,
1445        )
1446        assert isinstance(resp, dict)
1447        return resp
1448
1449    async def edit_clan(
1450        self,
1451        access_token: str,
1452        /,
1453        group_id: int,
1454        *,
1455        name: str | None = None,
1456        about: str | None = None,
1457        motto: str | None = None,
1458        theme: str | None = None,
1459        tags: collections.Sequence[str] | None = None,
1460        is_public: bool | None = None,
1461        locale: str | None = None,
1462        avatar_image_index: int | None = None,
1463        membership_option: enums.MembershipOption | int | None = None,
1464        allow_chat: bool | None = None,
1465        chat_security: typing.Literal[0, 1] | None = None,
1466        call_sign: str | None = None,
1467        homepage: typing.Literal[0, 1, 2] | None = None,
1468        enable_invite_messaging_for_admins: bool | None = None,
1469        default_publicity: typing.Literal[0, 1, 2] | None = None,
1470        is_public_topic_admin: bool | None = None,
1471    ) -> None:
1472        payload = {
1473            "name": name,
1474            "about": about,
1475            "motto": motto,
1476            "theme": theme,
1477            "tags": tags,
1478            "isPublic": is_public,
1479            "avatarImageIndex": avatar_image_index,
1480            "isPublicTopicAdminOnly": is_public_topic_admin,
1481            "allowChat": allow_chat,
1482            "chatSecurity": chat_security,
1483            "callsign": call_sign,
1484            "homepage": homepage,
1485            "enableInvitationMessagingForAdmins": enable_invite_messaging_for_admins,
1486            "defaultPublicity": default_publicity,
1487            "locale": locale,
1488        }
1489        if membership_option is not None:
1490            payload["membershipOption"] = int(membership_option)
1491
1492        await self._request(
1493            _POST,
1494            f"GroupV2/{group_id}/Edit",
1495            json=payload,
1496            auth=access_token,
1497        )
1498
1499    async def edit_clan_options(
1500        self,
1501        access_token: str,
1502        /,
1503        group_id: int,
1504        *,
1505        invite_permissions_override: bool | None = None,
1506        update_culture_permissionOverride: bool | None = None,
1507        host_guided_game_permission_override: typing.Literal[0, 1, 2] | None = None,
1508        update_banner_permission_override: bool | None = None,
1509        join_level: enums.ClanMemberType | int | None = None,
1510    ) -> None:
1511        payload = {
1512            "InvitePermissionOverride": invite_permissions_override,
1513            "UpdateCulturePermissionOverride": update_culture_permissionOverride,
1514            "HostGuidedGamePermissionOverride": host_guided_game_permission_override,
1515            "UpdateBannerPermissionOverride": update_banner_permission_override,
1516            "JoinLevel": int(join_level) if join_level else None,
1517        }
1518
1519        await self._request(
1520            _POST,
1521            f"GroupV2/{group_id}/EditFounderOptions",
1522            json=payload,
1523            auth=access_token,
1524        )
1525
1526    async def report_player(
1527        self,
1528        access_token: str,
1529        /,
1530        activity_id: int,
1531        character_id: int,
1532        reason_hashes: collections.Sequence[int],
1533        reason_category_hashes: collections.Sequence[int],
1534    ) -> None:
1535        await self._request(
1536            _POST,
1537            f"Destiny2/Stats/PostGameCarnageReport/{activity_id}/Report/",
1538            json={
1539                "reasonCategoryHashes": reason_category_hashes,
1540                "reasonHashes": reason_hashes,
1541                "offendingCharacterId": character_id,
1542            },
1543            auth=access_token,
1544        )
1545
1546    async def fetch_friends(self, access_token: str, /) -> typedefs.JSONObject:
1547        resp = await self._request(
1548            _GET,
1549            "Social/Friends/",
1550            auth=access_token,
1551        )
1552        assert isinstance(resp, dict)
1553        return resp
1554
1555    async def fetch_friend_requests(self, access_token: str, /) -> typedefs.JSONObject:
1556        resp = await self._request(
1557            _GET,
1558            "Social/Friends/Requests",
1559            auth=access_token,
1560        )
1561        assert isinstance(resp, dict)
1562        return resp
1563
1564    async def accept_friend_request(self, access_token: str, /, member_id: int) -> None:
1565        await self._request(
1566            _POST,
1567            f"Social/Friends/Requests/Accept/{member_id}",
1568            auth=access_token,
1569        )
1570
1571    async def send_friend_request(self, access_token: str, /, member_id: int) -> None:
1572        await self._request(
1573            _POST,
1574            f"Social/Friends/Add/{member_id}",
1575            auth=access_token,
1576        )
1577
1578    async def decline_friend_request(
1579        self, access_token: str, /, member_id: int
1580    ) -> None:
1581        await self._request(
1582            _POST,
1583            f"Social/Friends/Requests/Decline/{member_id}",
1584            auth=access_token,
1585        )
1586
1587    async def remove_friend(self, access_token: str, /, member_id: int) -> None:
1588        await self._request(
1589            _POST,
1590            f"Social/Friends/Remove/{member_id}",
1591            auth=access_token,
1592        )
1593
1594    async def remove_friend_request(self, access_token: str, /, member_id: int) -> None:
1595        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1596        await self._request(
1597            _POST,
1598            f"Social/Friends/Requests/Remove/{member_id}",
1599            auth=access_token,
1600        )
1601
1602    async def approve_all_pending_group_users(
1603        self,
1604        access_token: str,
1605        /,
1606        group_id: int,
1607        message: str | None = None,
1608    ) -> None:
1609        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1610        await self._request(
1611            _POST,
1612            f"GroupV2/{group_id}/Members/ApproveAll",
1613            auth=access_token,
1614            json={"message": str(message)},
1615        )
1616
1617    async def deny_all_pending_group_users(
1618        self,
1619        access_token: str,
1620        /,
1621        group_id: int,
1622        *,
1623        message: str | None = None,
1624    ) -> None:
1625        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1626        await self._request(
1627            _POST,
1628            f"GroupV2/{group_id}/Members/DenyAll",
1629            auth=access_token,
1630            json={"message": str(message)},
1631        )
1632
1633    async def add_optional_conversation(
1634        self,
1635        access_token: str,
1636        /,
1637        group_id: int,
1638        *,
1639        name: str | None = None,
1640        security: typing.Literal[0, 1] = 0,
1641    ) -> None:
1642        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1643        payload = {"chatName": str(name), "chatSecurity": security}
1644        await self._request(
1645            _POST,
1646            f"GroupV2/{group_id}/OptionalConversations/Add",
1647            json=payload,
1648            auth=access_token,
1649        )
1650
1651    async def edit_optional_conversation(
1652        self,
1653        access_token: str,
1654        /,
1655        group_id: int,
1656        conversation_id: int,
1657        *,
1658        name: str | None = None,
1659        security: typing.Literal[0, 1] = 0,
1660        enable_chat: bool = False,
1661    ) -> None:
1662        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1663        payload = {
1664            "chatEnabled": enable_chat,
1665            "chatName": str(name),
1666            "chatSecurity": security,
1667        }
1668        await self._request(
1669            _POST,
1670            f"GroupV2/{group_id}/OptionalConversations/Edit/{conversation_id}",
1671            json=payload,
1672            auth=access_token,
1673        )
1674
1675    async def transfer_item(
1676        self,
1677        access_token: str,
1678        /,
1679        item_id: int,
1680        item_hash: int,
1681        character_id: int,
1682        member_type: enums.MembershipType | int,
1683        *,
1684        stack_size: int = 1,
1685        vault: bool = False,
1686    ) -> None:
1687        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1688        payload = {
1689            "characterId": character_id,
1690            "membershipType": int(member_type),
1691            "itemId": item_id,
1692            "itemReferenceHash": item_hash,
1693            "stackSize": stack_size,
1694            "transferToVault": vault,
1695        }
1696        await self._request(
1697            _POST,
1698            "Destiny2/Actions/Items/TransferItem",
1699            json=payload,
1700            auth=access_token,
1701        )
1702
1703    async def pull_item(
1704        self,
1705        access_token: str,
1706        /,
1707        item_id: int,
1708        item_hash: int,
1709        character_id: int,
1710        member_type: enums.MembershipType | int,
1711        *,
1712        stack_size: int = 1,
1713        vault: bool = False,
1714    ) -> None:
1715        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1716        payload = {
1717            "characterId": character_id,
1718            "membershipType": int(member_type),
1719            "itemId": item_id,
1720            "itemReferenceHash": item_hash,
1721            "stackSize": stack_size,
1722        }
1723        await self._request(
1724            _POST,
1725            "Destiny2/Actions/Items/PullFromPostmaster",
1726            json=payload,
1727            auth=access_token,
1728        )
1729        if vault:
1730            await self.transfer_item(
1731                access_token,
1732                item_id=item_id,
1733                item_hash=item_hash,
1734                character_id=character_id,
1735                member_type=member_type,
1736                stack_size=stack_size,
1737                vault=True,
1738            )
1739
1740    async def fetch_fireteams(
1741        self,
1742        activity_type: fireteams.FireteamActivity | int,
1743        *,
1744        platform: fireteams.FireteamPlatform | int = fireteams.FireteamPlatform.ANY,
1745        language: fireteams.FireteamLanguage | str = fireteams.FireteamLanguage.ALL,
1746        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1747        page: int = 0,
1748        slots_filter: int = 0,
1749    ) -> typedefs.JSONObject:
1750        resp = await self._request(
1751            _GET,
1752            f"Fireteam/Search/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{page}/?langFilter={str(language)}",  # noqa: E501 Line too long
1753        )
1754        assert isinstance(resp, dict)
1755        return resp
1756
1757    async def fetch_available_clan_fireteams(
1758        self,
1759        access_token: str,
1760        group_id: int,
1761        activity_type: fireteams.FireteamActivity | int,
1762        *,
1763        platform: fireteams.FireteamPlatform | int,
1764        language: fireteams.FireteamLanguage | str,
1765        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1766        page: int = 0,
1767        public_only: bool = False,
1768        slots_filter: int = 0,
1769    ) -> typedefs.JSONObject:
1770        resp = await self._request(
1771            _GET,
1772            f"Fireteam/Clan/{group_id}/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{public_only}/{page}",  # noqa: E501
1773            json={"langFilter": str(language)},
1774            auth=access_token,
1775        )
1776        assert isinstance(resp, dict)
1777        return resp
1778
1779    async def fetch_clan_fireteam(
1780        self, access_token: str, fireteam_id: int, group_id: int
1781    ) -> typedefs.JSONObject:
1782        resp = await self._request(
1783            _GET,
1784            f"Fireteam/Clan/{group_id}/Summary/{fireteam_id}",
1785            auth=access_token,
1786        )
1787        assert isinstance(resp, dict)
1788        return resp
1789
1790    async def fetch_my_clan_fireteams(
1791        self,
1792        access_token: str,
1793        group_id: int,
1794        *,
1795        include_closed: bool = True,
1796        platform: fireteams.FireteamPlatform | int,
1797        language: fireteams.FireteamLanguage | str,
1798        filtered: bool = True,
1799        page: int = 0,
1800    ) -> typedefs.JSONObject:
1801        payload = {"groupFilter": filtered, "langFilter": str(language)}
1802
1803        resp = await self._request(
1804            _GET,
1805            f"Fireteam/Clan/{group_id}/My/{int(platform)}/{include_closed}/{page}",
1806            json=payload,
1807            auth=access_token,
1808        )
1809        assert isinstance(resp, dict)
1810        return resp
1811
1812    async def fetch_private_clan_fireteams(
1813        self, access_token: str, group_id: int, /
1814    ) -> int:
1815        resp = await self._request(
1816            _GET,
1817            f"Fireteam/Clan/{group_id}/ActiveCount",
1818            auth=access_token,
1819        )
1820        assert isinstance(resp, int)
1821        return resp
1822
1823    async def fetch_post_activity(self, instance_id: int, /) -> typedefs.JSONObject:
1824        resp = await self._request(
1825            _GET, f"Destiny2/Stats/PostGameCarnageReport/{instance_id}"
1826        )
1827        assert isinstance(resp, dict)
1828        return resp
1829
1830    @helpers.unstable
1831    async def search_entities(
1832        self, name: str, entity_type: str, *, page: int = 0
1833    ) -> typedefs.JSONObject:
1834        resp = await self._request(
1835            _GET,
1836            f"Destiny2/Armory/Search/{entity_type}/{name}/",
1837            json={"page": page},
1838        )
1839        assert isinstance(resp, dict)
1840        return resp
1841
1842    async def fetch_unique_weapon_history(
1843        self,
1844        membership_id: int,
1845        character_id: int,
1846        membership_type: enums.MembershipType | int,
1847    ) -> typedefs.JSONObject:
1848        resp = await self._request(
1849            _GET,
1850            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/UniqueWeapons/",
1851        )
1852        assert isinstance(resp, dict)
1853        return resp
1854
1855    async def fetch_item(
1856        self,
1857        member_id: int,
1858        item_id: int,
1859        membership_type: enums.MembershipType | int,
1860        components: collections.Sequence[enums.ComponentType],
1861    ) -> typedefs.JSONObject:
1862        collector = _collect_components(components)
1863
1864        resp = await self._request(
1865            _GET,
1866            f"Destiny2/{int(membership_type)}/Profile/{member_id}/Item/{item_id}/?components={collector}",
1867        )
1868        assert isinstance(resp, dict)
1869        return resp
1870
1871    async def fetch_clan_weekly_rewards(self, clan_id: int, /) -> typedefs.JSONObject:
1872        resp = await self._request(_GET, f"Destiny2/Clan/{clan_id}/WeeklyRewardState/")
1873        assert isinstance(resp, dict)
1874        return resp
1875
1876    async def fetch_available_locales(self) -> typedefs.JSONObject:
1877        resp = await self._request(_GET, "Destiny2/Manifest/DestinyLocaleDefinition/")
1878        assert isinstance(resp, dict)
1879        return resp
1880
1881    async def fetch_common_settings(self) -> typedefs.JSONObject:
1882        resp = await self._request(_GET, "Settings")
1883        assert isinstance(resp, dict)
1884        return resp
1885
1886    async def fetch_user_systems_overrides(self) -> typedefs.JSONObject:
1887        resp = await self._request(_GET, "UserSystemOverrides")
1888        assert isinstance(resp, dict)
1889        return resp
1890
1891    async def fetch_global_alerts(
1892        self, *, include_streaming: bool = False
1893    ) -> typedefs.JSONArray:
1894        resp = await self._request(
1895            _GET, f"GlobalAlerts/?includestreaming={include_streaming}"
1896        )
1897        assert isinstance(resp, list)
1898        return resp
1899
1900    async def awainitialize_request(
1901        self,
1902        access_token: str,
1903        type: typing.Literal[0, 1],
1904        membership_type: enums.MembershipType | int,
1905        /,
1906        *,
1907        affected_item_id: int | None = None,
1908        character_id: int | None = None,
1909    ) -> typedefs.JSONObject:
1910        body = {"type": type, "membershipType": int(membership_type)}
1911
1912        if affected_item_id is not None:
1913            body["affectedItemId"] = affected_item_id
1914
1915        if character_id is not None:
1916            body["characterId"] = character_id
1917
1918        resp = await self._request(
1919            _POST, "Destiny2/Awa/Initialize", json=body, auth=access_token
1920        )
1921        assert isinstance(resp, dict)
1922        return resp
1923
1924    async def awaget_action_token(
1925        self, access_token: str, correlation_id: str, /
1926    ) -> typedefs.JSONObject:
1927        resp = await self._request(
1928            _POST,
1929            f"Destiny2/Awa/GetActionToken/{correlation_id}",
1930            auth=access_token,
1931        )
1932        assert isinstance(resp, dict)
1933        return resp
1934
1935    async def awa_provide_authorization_result(
1936        self,
1937        access_token: str,
1938        selection: int,
1939        correlation_id: str,
1940        nonce: collections.MutableSequence[str | bytes],
1941    ) -> int:
1942        body = {"selection": selection, "correlationId": correlation_id, "nonce": nonce}
1943
1944        resp = await self._request(
1945            _POST,
1946            "Destiny2/Awa/AwaProvideAuthorizationResult",
1947            json=body,
1948            auth=access_token,
1949        )
1950        assert isinstance(resp, int)
1951        return resp
1952
1953    async def fetch_vendors(
1954        self,
1955        access_token: str,
1956        character_id: int,
1957        membership_id: int,
1958        membership_type: enums.MembershipType | int,
1959        /,
1960        components: collections.Sequence[enums.ComponentType],
1961        filter: int | None = None,
1962    ) -> typedefs.JSONObject:
1963        components_ = _collect_components(components)
1964        route = (
1965            f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1966            f"/Character/{character_id}/Vendors/?components={components_}"
1967        )
1968
1969        if filter is not None:
1970            route = route + f"&filter={filter}"
1971
1972        resp = await self._request(
1973            _GET,
1974            route,
1975            auth=access_token,
1976        )
1977        assert isinstance(resp, dict)
1978        return resp
1979
1980    async def fetch_vendor(
1981        self,
1982        access_token: str,
1983        character_id: int,
1984        membership_id: int,
1985        membership_type: enums.MembershipType | int,
1986        vendor_hash: int,
1987        /,
1988        components: collections.Sequence[enums.ComponentType],
1989    ) -> typedefs.JSONObject:
1990        components_ = _collect_components(components)
1991        resp = await self._request(
1992            _GET,
1993            (
1994                f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1995                f"/Character/{character_id}/Vendors/{vendor_hash}/?components={components_}"
1996            ),
1997            auth=access_token,
1998        )
1999        assert isinstance(resp, dict)
2000        return resp
2001
2002    async def fetch_application_api_usage(
2003        self,
2004        access_token: str,
2005        application_id: int,
2006        /,
2007        *,
2008        start: datetime.datetime | None = None,
2009        end: datetime.datetime | None = None,
2010    ) -> typedefs.JSONObject:
2011        end_date, start_date = time.parse_date_range(end, start)
2012        resp = await self._request(
2013            _GET,
2014            f"App/ApiUsage/{application_id}/?end={end_date}&start={start_date}",
2015            auth=access_token,
2016        )
2017        assert isinstance(resp, dict)
2018        return resp
2019
2020    async def fetch_bungie_applications(self) -> typedefs.JSONArray:
2021        resp = await self._request(_GET, "App/FirstParty")
2022        assert isinstance(resp, list)
2023        return resp
2024
2025    async def fetch_content_type(self, type: str, /) -> typedefs.JSONObject:
2026        resp = await self._request(_GET, f"Content/GetContentType/{type}/")
2027        assert isinstance(resp, dict)
2028        return resp
2029
2030    async def fetch_content_by_id(
2031        self, id: int, locale: str, /, *, head: bool = False
2032    ) -> typedefs.JSONObject:
2033        resp = await self._request(
2034            _GET,
2035            f"Content/GetContentById/{id}/{locale}/",
2036            json={"head": head},
2037        )
2038        assert isinstance(resp, dict)
2039        return resp
2040
2041    async def fetch_content_by_tag_and_type(
2042        self, locale: str, tag: str, type: str, *, head: bool = False
2043    ) -> typedefs.JSONObject:
2044        resp = await self._request(
2045            _GET,
2046            f"Content/GetContentByTagAndType/{tag}/{type}/{locale}/",
2047            json={"head": head},
2048        )
2049        assert isinstance(resp, dict)
2050        return resp
2051
2052    async def search_content_with_text(
2053        self,
2054        locale: str,
2055        /,
2056        content_type: str,
2057        search_text: str,
2058        tag: str,
2059        *,
2060        page: int | None = None,
2061        source: str | None = None,
2062    ) -> typedefs.JSONObject:
2063        body: typedefs.JSONObject = {
2064            "locale": locale,
2065            "currentpage": page or 1,
2066            "ctype": content_type,
2067            "searchtxt": search_text,
2068            "searchtext": search_text,
2069            "tag": tag,
2070            "source": source,
2071        }
2072
2073        resp = await self._request(_GET, "Content/Search", params=body)
2074        assert isinstance(resp, dict)
2075        return resp
2076
2077    async def search_content_by_tag_and_type(
2078        self,
2079        locale: str,
2080        tag: str,
2081        type: str,
2082        *,
2083        page: int | None = None,
2084    ) -> typedefs.JSONObject:
2085        body: typedefs.JSONObject = {"currentpage": page or 1}
2086
2087        resp = await self._request(
2088            _GET,
2089            f"Content/SearchContentByTagAndType/{tag}/{type}/{locale}/",
2090            params=body,
2091        )
2092        assert isinstance(resp, dict)
2093        return resp
2094
2095    async def search_help_articles(
2096        self, text: str, size: str, /
2097    ) -> typedefs.JSONObject:
2098        resp = await self._request(_GET, f"Content/SearchHelpArticles/{text}/{size}/")
2099        assert isinstance(resp, dict)
2100        return resp
2101
2102    async def fetch_topics_page(
2103        self,
2104        category_filter: int,
2105        group: int,
2106        date_filter: int,
2107        sort: str | bytes,
2108        *,
2109        page: int | None = None,
2110        locales: collections.Iterable[str] | None = None,
2111        tag_filter: str | None = None,
2112    ) -> typedefs.JSONObject:
2113        params = {
2114            "locales": ",".join(locales) if locales is not None else "en",
2115        }
2116        if tag_filter:
2117            params["tagstring"] = tag_filter
2118
2119        resp = await self._request(
2120            _GET,
2121            f"Forum/GetTopicsPaged/{page or 0}/0/{group}/{sort!s}/{date_filter}/{category_filter}/",
2122            params=params,
2123        )
2124        assert isinstance(resp, dict)
2125        return resp
2126
2127    async def fetch_core_topics_page(
2128        self,
2129        category_filter: int,
2130        date_filter: int,
2131        sort: str | bytes,
2132        *,
2133        page: int | None = None,
2134        locales: collections.Iterable[str] | None = None,
2135    ) -> typedefs.JSONObject:
2136        resp = await self._request(
2137            _GET,
2138            f"Forum/GetCoreTopicsPaged/{page or 0}"
2139            f"/{sort!s}/{date_filter}/{category_filter}/?locales={','.join(locales) if locales else 'en'}",
2140        )
2141        assert isinstance(resp, dict)
2142        return resp
2143
2144    async def fetch_posts_threaded_page(
2145        self,
2146        parent_post: bool,
2147        page: int,
2148        page_size: int,
2149        parent_post_id: int,
2150        reply_size: int,
2151        root_thread_mode: bool,
2152        sort_mode: int,
2153        show_banned: str | None = None,
2154    ) -> typedefs.JSONObject:
2155        resp = await self._request(
2156            _GET,
2157            f"Forum/GetPostsThreadedPaged/{parent_post}/{page}/"
2158            f"{page_size}/{reply_size}/{parent_post_id}/{root_thread_mode}/{sort_mode}/",
2159            json={"showbanned": show_banned},
2160        )
2161        assert isinstance(resp, dict)
2162        return resp
2163
2164    async def fetch_posts_threaded_page_from_child(
2165        self,
2166        child_id: bool,
2167        page: int,
2168        page_size: int,
2169        reply_size: int,
2170        root_thread_mode: bool,
2171        sort_mode: int,
2172        show_banned: str | None = None,
2173    ) -> typedefs.JSONObject:
2174        resp = await self._request(
2175            _GET,
2176            f"Forum/GetPostsThreadedPagedFromChild/{child_id}/"
2177            f"{page}/{page_size}/{reply_size}/{root_thread_mode}/{sort_mode}/",
2178            json={"showbanned": show_banned},
2179        )
2180        assert isinstance(resp, dict)
2181        return resp
2182
2183    async def fetch_post_and_parent(
2184        self, child_id: int, /, *, show_banned: str | None = None
2185    ) -> typedefs.JSONObject:
2186        resp = await self._request(
2187            _GET,
2188            f"Forum/GetPostAndParent/{child_id}/",
2189            json={"showbanned": show_banned},
2190        )
2191        assert isinstance(resp, dict)
2192        return resp
2193
2194    async def fetch_posts_and_parent_awaiting(
2195        self, child_id: int, /, *, show_banned: str | None = None
2196    ) -> typedefs.JSONObject:
2197        resp = await self._request(
2198            _GET,
2199            f"Forum/GetPostAndParentAwaitingApproval/{child_id}/",
2200            json={"showbanned": show_banned},
2201        )
2202        assert isinstance(resp, dict)
2203        return resp
2204
2205    async def fetch_topic_for_content(self, content_id: int, /) -> int:
2206        resp = await self._request(_GET, f"Forum/GetTopicForContent/{content_id}/")
2207        assert isinstance(resp, int)
2208        return resp
2209
2210    async def fetch_forum_tag_suggestions(
2211        self, partial_tag: str, /
2212    ) -> typedefs.JSONObject:
2213        resp = await self._request(
2214            _GET,
2215            "Forum/GetForumTagSuggestions/",
2216            json={"partialtag": partial_tag},
2217        )
2218        assert isinstance(resp, dict)
2219        return resp
2220
2221    async def fetch_poll(self, topic_id: int, /) -> typedefs.JSONObject:
2222        resp = await self._request(_GET, f"Forum/Poll/{topic_id}/")
2223        assert isinstance(resp, dict)
2224        return resp
2225
2226    async def fetch_recruitment_thread_summaries(self) -> typedefs.JSONArray:
2227        resp = await self._request(_POST, "Forum/Recruit/Summaries/")
2228        assert isinstance(resp, list)
2229        return resp
2230
2231    async def fetch_recommended_groups(
2232        self,
2233        access_token: str,
2234        /,
2235        *,
2236        date_range: int = 0,
2237        group_type: enums.GroupType | int = enums.GroupType.CLAN,
2238    ) -> typedefs.JSONArray:
2239        resp = await self._request(
2240            _POST,
2241            f"GroupV2/Recommended/{int(group_type)}/{date_range}/",
2242            auth=access_token,
2243        )
2244        assert isinstance(resp, list)
2245        return resp
2246
2247    async def fetch_available_avatars(self) -> collections.Mapping[str, int]:
2248        resp = await self._request(_GET, "GroupV2/GetAvailableAvatars/")
2249        assert isinstance(resp, dict)
2250        return resp
2251
2252    async def fetch_user_clan_invite_setting(
2253        self,
2254        access_token: str,
2255        /,
2256        membership_type: enums.MembershipType | int,
2257    ) -> bool:
2258        resp = await self._request(
2259            _GET,
2260            f"GroupV2/GetUserClanInviteSetting/{int(membership_type)}/",
2261            auth=access_token,
2262        )
2263        assert isinstance(resp, bool)
2264        return resp
2265
2266    async def fetch_banned_group_members(
2267        self, access_token: str, group_id: int, /, *, page: int = 1
2268    ) -> typedefs.JSONObject:
2269        resp = await self._request(
2270            _GET,
2271            f"GroupV2/{group_id}/Banned/?currentpage={page}",
2272            auth=access_token,
2273        )
2274        assert isinstance(resp, dict)
2275        return resp
2276
2277    async def fetch_pending_group_memberships(
2278        self, access_token: str, group_id: int, /, *, current_page: int = 1
2279    ) -> typedefs.JSONObject:
2280        resp = await self._request(
2281            _GET,
2282            f"GroupV2/{group_id}/Members/Pending/?currentpage={current_page}",
2283            auth=access_token,
2284        )
2285        assert isinstance(resp, dict)
2286        return resp
2287
2288    async def fetch_invited_group_memberships(
2289        self, access_token: str, group_id: int, /, *, current_page: int = 1
2290    ) -> typedefs.JSONObject:
2291        resp = await self._request(
2292            _GET,
2293            f"GroupV2/{group_id}/Members/InvitedIndividuals/?currentpage={current_page}",
2294            auth=access_token,
2295        )
2296        assert isinstance(resp, dict)
2297        return resp
2298
2299    async def invite_member_to_group(
2300        self,
2301        access_token: str,
2302        /,
2303        group_id: int,
2304        membership_id: int,
2305        membership_type: enums.MembershipType | int,
2306        *,
2307        message: str | None = None,
2308    ) -> typedefs.JSONObject:
2309        resp = await self._request(
2310            _POST,
2311            f"GroupV2/{group_id}/Members/IndividualInvite/{int(membership_type)}/{membership_id}/",
2312            auth=access_token,
2313            json={"message": str(message)},
2314        )
2315        assert isinstance(resp, dict)
2316        return resp
2317
2318    async def cancel_group_member_invite(
2319        self,
2320        access_token: str,
2321        /,
2322        group_id: int,
2323        membership_id: int,
2324        membership_type: enums.MembershipType | int,
2325    ) -> typedefs.JSONObject:
2326        resp = await self._request(
2327            _POST,
2328            f"GroupV2/{group_id}/Members/IndividualInviteCancel/{int(membership_type)}/{membership_id}/",
2329            auth=access_token,
2330        )
2331        assert isinstance(resp, dict)
2332        return resp
2333
2334    async def fetch_historical_definition(self) -> typedefs.JSONObject:
2335        resp = await self._request(_GET, "Destiny2/Stats/Definition/")
2336        assert isinstance(resp, dict)
2337        return resp
2338
2339    async def fetch_historical_stats(
2340        self,
2341        character_id: int,
2342        membership_id: int,
2343        membership_type: enums.MembershipType | int,
2344        day_start: datetime.datetime,
2345        day_end: datetime.datetime,
2346        groups: collections.Sequence[enums.StatsGroupType | int],
2347        modes: collections.Sequence[enums.GameMode | int],
2348        *,
2349        period_type: enums.PeriodType = enums.PeriodType.ALL_TIME,
2350    ) -> typedefs.JSONObject:
2351        end, start = time.parse_date_range(day_end, day_start)
2352        resp = await self._request(
2353            _GET,
2354            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/",
2355            json={
2356                "dayend": end,
2357                "daystart": start,
2358                "groups": [str(int(group)) for group in groups],
2359                "modes": [str(int(mode)) for mode in modes],
2360                "periodType": int(period_type),
2361            },
2362        )
2363        assert isinstance(resp, dict)
2364        return resp
2365
2366    async def fetch_historical_stats_for_account(
2367        self,
2368        membership_id: int,
2369        membership_type: enums.MembershipType | int,
2370        groups: collections.Sequence[enums.StatsGroupType | int],
2371    ) -> typedefs.JSONObject:
2372        resp = await self._request(
2373            _GET,
2374            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Stats/",
2375            json={"groups": [str(int(group)) for group in groups]},
2376        )
2377        assert isinstance(resp, dict)
2378        return resp
2379
2380    async def fetch_aggregated_activity_stats(
2381        self,
2382        character_id: int,
2383        membership_id: int,
2384        membership_type: enums.MembershipType | int,
2385        /,
2386    ) -> typedefs.JSONObject:
2387        resp = await self._request(
2388            _GET,
2389            f"Destiny2/{int(membership_type)}/Account/{membership_id}/"
2390            f"Character/{character_id}/Stats/AggregateActivityStats/",
2391        )
2392        assert isinstance(resp, dict)
2393        return resp
2394
2395    async def equip_loadout(
2396        self,
2397        access_token: str,
2398        /,
2399        loadout_index: int,
2400        character_id: int,
2401        membership_type: enums.MembershipType | int,
2402    ) -> None:
2403        response = await self._request(
2404            _POST,
2405            "Destiny2/Actions/Loadouts/EquipLoadout/",
2406            json={
2407                "loadoutIndex": loadout_index,
2408                "characterId": character_id,
2409                "membership_type": int(membership_type),
2410            },
2411            auth=access_token,
2412        )
2413        assert isinstance(response, int)
2414
2415    async def snapshot_loadout(
2416        self,
2417        access_token: str,
2418        /,
2419        loadout_index: int,
2420        character_id: int,
2421        membership_type: enums.MembershipType | int,
2422        *,
2423        color_hash: int | None = None,
2424        icon_hash: int | None = None,
2425        name_hash: int | None = None,
2426    ) -> None:
2427        response = await self._request(
2428            _POST,
2429            "Destiny2/Actions/Loadouts/SnapshotLoadout/",
2430            auth=access_token,
2431            json={
2432                "colorHash": color_hash,
2433                "iconHash": icon_hash,
2434                "nameHash": name_hash,
2435                "loadoutIndex": loadout_index,
2436                "characterId": character_id,
2437                "membershipType": int(membership_type),
2438            },
2439        )
2440        assert isinstance(response, int)
2441
2442    async def update_loadout(
2443        self,
2444        access_token: str,
2445        /,
2446        loadout_index: int,
2447        character_id: int,
2448        membership_type: enums.MembershipType | int,
2449        *,
2450        color_hash: int | None = None,
2451        icon_hash: int | None = None,
2452        name_hash: int | None = None,
2453    ) -> None:
2454        response = await self._request(
2455            _POST,
2456            "Destiny2/Actions/Loadouts/UpdateLoadoutIdentifiers/",
2457            auth=access_token,
2458            json={
2459                "colorHash": color_hash,
2460                "iconHash": icon_hash,
2461                "nameHash": name_hash,
2462                "loadoutIndex": loadout_index,
2463                "characterId": character_id,
2464                "membershipType": int(membership_type),
2465            },
2466        )
2467        assert isinstance(response, int)
2468
2469    async def clear_loadout(
2470        self,
2471        access_token: str,
2472        /,
2473        loadout_index: int,
2474        character_id: int,
2475        membership_type: enums.MembershipType | int,
2476    ) -> None:
2477        response = await self._request(
2478            _POST,
2479            "Destiny2/Actions/Loadouts/ClearLoadout/",
2480            json={
2481                "loadoutIndex": loadout_index,
2482                "characterId": character_id,
2483                "membership_type": int(membership_type),
2484            },
2485            auth=access_token,
2486        )
2487        assert isinstance(response, int)
2488
2489    async def force_drops_repair(self, access_token: str, /) -> bool:
2490        response = await self._request(
2491            _POST, "Tokens/Partner/ForceDropsRepair/", auth=access_token
2492        )
2493        assert isinstance(response, bool)
2494        return response
2495
2496    async def claim_partner_offer(
2497        self,
2498        access_token: str,
2499        /,
2500        *,
2501        offer_id: str,
2502        bungie_membership_id: int,
2503        transaction_id: str,
2504    ) -> bool:
2505        response = await self._request(
2506            _POST,
2507            "Tokens/Partner/ClaimOffer/",
2508            json={
2509                "PartnerOfferId": offer_id,
2510                "BungieNetMembershipId": bungie_membership_id,
2511                "TransactionId": transaction_id,
2512            },
2513            auth=access_token,
2514        )
2515        assert isinstance(response, bool)
2516        return response
2517
2518    async def fetch_bungie_rewards_for_user(
2519        self, access_token: str, /, membership_id: int
2520    ) -> typedefs.JSONObject:
2521        response = await self._request(
2522            _GET,
2523            f"Tokens/Rewards/GetRewardsForUser/{membership_id}/",
2524            auth=access_token,
2525        )
2526        assert isinstance(response, dict)
2527        return response
2528
2529    async def fetch_bungie_rewards_for_platform(
2530        self,
2531        access_token: str,
2532        /,
2533        membership_id: int,
2534        membership_type: enums.MembershipType | int,
2535    ) -> typedefs.JSONObject:
2536        response = await self._request(
2537            _GET,
2538            f"Tokens/Rewards/GetRewardsForPlatformUser/{membership_id}/{int(membership_type)}",
2539            auth=access_token,
2540        )
2541        assert isinstance(response, dict)
2542        return response
2543
2544    async def fetch_bungie_rewards(self) -> typedefs.JSONObject:
2545        response = await self._request(_GET, "Tokens/Rewards/BungieRewards/")
2546        assert isinstance(response, dict)
2547        return response
2548
2549    async def fetch_fireteam_listing(self, listing_id: int) -> typedefs.JSONObject:
2550        response = await self._request(_GET, f"FireteamFinder/Listing/{listing_id}/")
2551        assert isinstance(response, dict)
2552        return response
class RESTClient(aiobungie.api.rest.RESTClient):
 432class RESTClient(api.RESTClient):
 433    """A single process REST client implementation.
 434
 435    This client is designed to only make HTTP requests and return raw JSON objects.
 436
 437    Example
 438    -------
 439    ```py
 440    import aiobungie
 441
 442    client = aiobungie.RESTClient("TOKEN")
 443    async with client:
 444        response = await client.fetch_clan_members(4389205)
 445        for member in response['results']:
 446            print(member['destinyUserInfo'])
 447    ```
 448
 449    Parameters
 450    ----------
 451    token : `str`
 452        A valid application token from Bungie's developer portal.
 453
 454    Other Parameters
 455    ----------------
 456    client_secret : `str | None`
 457        An optional application client secret,
 458        This is only needed if you're fetching OAuth2 tokens with this client.
 459    client_id : `int | None`
 460        An optional application client id,
 461        This is only needed if you're fetching OAuth2 tokens with this client.
 462    settings: `aiobungie.builders.Settings | None`
 463        The client settings to use, if `None` the default will be used.
 464    owned_client: `bool`
 465        * If set to `True`, this client will use the provided `client_session` parameter instead,
 466        * If set to `True` and `client_session` is `None`, `ValueError` will be raised.
 467        * If set to `False`, aiobungie will initialize a new client session for you.
 468
 469    client_session: `aiohttp.ClientSession | None`
 470        If provided, this client session will be used to make all the HTTP requests.
 471        The `owned_client` must be set to `True` for this to work.
 472    max_retries : `int`
 473        The max retries number to retry if the request hit a `5xx` status code.
 474    debug : `bool | str`
 475        Whether to enable logging responses or not.
 476
 477    Logging Levels
 478    --------------
 479    * `False`: This will disable logging.
 480    * `True`: This will set the level to `DEBUG` and enable logging minimal information.
 481    * `"TRACE" | aiobungie.TRACE`: This will log the response headers along with the minimal information.
 482    """
 483
 484    __slots__ = (
 485        "_token",
 486        "_session",
 487        "_lock",
 488        "_max_retries",
 489        "_client_secret",
 490        "_client_id",
 491        "_metadata",
 492        "_dumps",
 493        "_loads",
 494        "_owned_client",
 495        "_settings",
 496    )
 497
 498    def __init__(
 499        self,
 500        token: str,
 501        /,
 502        *,
 503        client_secret: str | None = None,
 504        client_id: int | None = None,
 505        settings: builders.Settings | None = None,
 506        owned_client: bool = True,
 507        client_session: aiohttp.ClientSession | None = None,
 508        dumps: typedefs.Dumps = helpers.dumps,
 509        loads: typedefs.Loads = helpers.loads,
 510        max_retries: int = 4,
 511        debug: typing.Literal["TRACE"] | bool | int = False,
 512    ) -> None:
 513        if owned_client is False and client_session is None:
 514            raise ValueError(
 515                "Expected an owned client session, but got `None`, Cannot have `owned_client` set to `False` and `client_session` to `None`"
 516            )
 517
 518        self._settings = settings or builders.Settings()
 519        self._session = client_session
 520        self._owned_client = owned_client
 521        self._lock: asyncio.Lock | None = None
 522        self._client_secret = client_secret
 523        self._client_id = client_id
 524        self._token: str = token
 525        self._max_retries = max_retries
 526        self._dumps = dumps
 527        self._loads = loads
 528        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
 529        self.with_debug(debug)
 530
 531    @property
 532    def client_id(self) -> int | None:
 533        return self._client_id
 534
 535    @property
 536    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
 537        return self._metadata
 538
 539    @property
 540    def is_alive(self) -> bool:
 541        return self._session is not None
 542
 543    @property
 544    def settings(self) -> builders.Settings:
 545        return self._settings
 546
 547    async def close(self) -> None:
 548        if self._session is None:
 549            raise RuntimeError("REST client is not running.")
 550
 551        if self._owned_client:
 552            await self._session.close()
 553            self._session = None
 554
 555    def open(self) -> None:
 556        """Open a new client session. This is called internally with contextmanager usage."""
 557        if self.is_alive and self._owned_client:
 558            raise RuntimeError("Cannot open REST client when it's already open.")
 559
 560        if self._owned_client:
 561            self._session = aiohttp.ClientSession(
 562                connector=aiohttp.TCPConnector(
 563                    use_dns_cache=self._settings.use_dns_cache,
 564                    ttl_dns_cache=self._settings.ttl_dns_cache,
 565                    ssl_context=self._settings.ssl_context,
 566                    ssl=self._settings.ssl,
 567                ),
 568                connector_owner=True,
 569                raise_for_status=False,
 570                timeout=self._settings.http_timeout,
 571                trust_env=self._settings.trust_env,
 572                headers=self._settings.headers,
 573            )
 574
 575    @typing.final
 576    async def static_request(
 577        self,
 578        method: _HTTP_METHOD,
 579        path: str,
 580        *,
 581        auth: str | None = None,
 582        json: collections.Mapping[str, typing.Any] | None = None,
 583        params: collections.Mapping[str, typing.Any] | None = None,
 584    ) -> typedefs.JSONIsh:
 585        return await self._request(method, path, auth=auth, json=json, params=params)
 586
 587    @typing.overload
 588    def build_oauth2_url(self, client_id: int) -> builders.OAuthURL: ...
 589
 590    @typing.overload
 591    def build_oauth2_url(self) -> builders.OAuthURL | None: ...
 592
 593    @typing.final
 594    def build_oauth2_url(
 595        self, client_id: int | None = None
 596    ) -> builders.OAuthURL | None:
 597        client_id = client_id or self._client_id
 598        if client_id is None:
 599            return None
 600
 601        return builders.OAuthURL(client_id=client_id)
 602
 603    @typing.final
 604    async def _request(
 605        self,
 606        method: _HTTP_METHOD,
 607        route: str,
 608        *,
 609        base: bool = False,
 610        oauth2: bool = False,
 611        auth: str | None = None,
 612        unwrap_bytes: bool = False,
 613        json: collections.Mapping[str, typing.Any] | None = None,
 614        data: collections.Mapping[str, typing.Any] | None = None,
 615        params: collections.Mapping[str, typing.Any] | None = None,
 616    ) -> typedefs.JSONIsh:
 617        # This is not None when opening the client.
 618        assert self._session is not None, (
 619            "This client hasn't been opened yet. Use `async with client` or `async with client.rest` "
 620            "before performing any request."
 621        )
 622
 623        retries: int = 0
 624        headers: collections.MutableMapping[str, typing.Any] = {}
 625
 626        headers[_USER_AGENT_HEADERS] = _USER_AGENT
 627        headers["X-API-KEY"] = self._token
 628
 629        if auth is not None:
 630            headers[_AUTH_HEADER] = f"Bearer {auth}"
 631
 632        # Handling endpoints
 633        endpoint = url.BASE
 634
 635        if not base:
 636            endpoint = endpoint + url.REST_EP
 637
 638        if oauth2:
 639            assert self._client_id, "Client ID is required to make authorized requests."
 640            assert (
 641                self._client_secret
 642            ), "Client secret is required to make authorized requests."
 643            headers["client_secret"] = self._client_secret
 644
 645            headers["Content-Type"] = "application/x-www-form-urlencoded"
 646            endpoint = endpoint + url.TOKEN_EP
 647
 648        if self._lock is None:
 649            self._lock = asyncio.Lock()
 650
 651        if json:
 652            headers["Content-Type"] = _APP_JSON
 653
 654        stack = contextlib.AsyncExitStack()
 655        while True:
 656            try:
 657                await stack.enter_async_context(self._lock)
 658
 659                # We make the request here.
 660                taken_time = time.monotonic()
 661                response = await self._session.request(
 662                    method=method,
 663                    url=f"{endpoint}/{route}",
 664                    headers=headers,
 665                    data=_JSONPayload(json) if json else data,
 666                    params=params,
 667                )
 668                response_time = (time.monotonic() - taken_time) * 1_000
 669
 670                _LOGGER.debug(
 671                    "METHOD: %s ROUTE: %s STATUS: %i ELAPSED: %.4fms",
 672                    method,
 673                    f"{endpoint}/{route}",
 674                    response.status,
 675                    response_time,
 676                )
 677
 678                await self._handle_ratelimit(response, method, route)
 679
 680            except aiohttp.ClientConnectionError as exc:
 681                if retries >= self._max_retries:
 682                    raise error.HTTPError(
 683                        str(exc),
 684                        http.HTTPStatus.SERVICE_UNAVAILABLE,
 685                    )
 686                backoff_ = backoff.ExponentialBackOff(maximum=8)
 687
 688                timer = next(backoff_)
 689                _LOGGER.warning(
 690                    "Client received a connection error <%s> Retrying in %.2fs. Remaining retries: %s",
 691                    type(exc).__qualname__,
 692                    timer,
 693                    self._max_retries - retries,
 694                )
 695                retries += 1
 696                await asyncio.sleep(timer)
 697                continue
 698
 699            finally:
 700                await stack.aclose()
 701
 702            if response.status == http.HTTPStatus.NO_CONTENT:
 703                return None
 704
 705            # Handle the successful response.
 706            if 300 > response.status >= 200:
 707                if unwrap_bytes:
 708                    # We need to read the bytes for the manifest response.
 709                    return await response.read()
 710
 711                # Bungie get funky and return HTML instead of JSON when making an authorized
 712                # request with a dummy access token. We could technically read the page content
 713                # but that's Bungie's fault for not returning a JSON response.
 714                if response.content_type != _APP_JSON:
 715                    raise error.HTTPError(
 716                        message=f"Expected JSON response, Got {response.content_type}, "
 717                        f"{response.real_url.human_repr()}",
 718                        http_status=http.HTTPStatus(response.status),
 719                    )
 720
 721                json_data = self._loads(await response.read())
 722
 723                if _LOGGER.isEnabledFor(TRACE):
 724                    _LOGGER.log(
 725                        TRACE,
 726                        "%s",
 727                        error.stringify_headers(dict(response.headers)),
 728                    )
 729
 730                    details: collections.MutableMapping[str, typing.Any] = {}
 731                    if json:
 732                        details["json"] = error.filtered_headers(json)
 733
 734                    if data:
 735                        details["data"] = error.filtered_headers(data)
 736
 737                    if params:
 738                        details["params"] = error.filtered_headers(params)
 739
 740                    if details:
 741                        _LOGGER.log(TRACE, "%s", error.stringify_headers(details))
 742
 743                # Return the response.
 744                # auth responses are not inside a Response object.
 745                if oauth2:
 746                    return json_data
 747
 748                # The reason we have a type ignore is because the actual response type
 749                # is within this `Response` key.
 750                return json_data["Response"]  # type: ignore
 751
 752            if (
 753                response.status in _RETRY_5XX and retries < self._max_retries  # noqa: W503
 754            ):
 755                backoff_ = backoff.ExponentialBackOff(maximum=6)
 756                sleep_time = next(backoff_)
 757                _LOGGER.warning(
 758                    "Got %i - %s. Sleeping for %.2f seconds. Remaining retries: %i",
 759                    response.status,
 760                    response.reason,
 761                    sleep_time,
 762                    self._max_retries - retries,
 763                )
 764
 765                retries += 1
 766                await asyncio.sleep(sleep_time)
 767                continue
 768
 769            raise await error.panic(response)
 770
 771    async def __aenter__(self) -> RESTClient:
 772        self.open()
 773        return self
 774
 775    async def __aexit__(
 776        self,
 777        exception_type: type[BaseException] | None,
 778        exception: BaseException | None,
 779        exception_traceback: types.TracebackType | None,
 780    ) -> None:
 781        await self.close()
 782
 783    # We don't want this to be super complicated.
 784    async def _handle_ratelimit(
 785        self,
 786        response: aiohttp.ClientResponse,
 787        method: str,
 788        route: str,
 789    ) -> None:
 790        if response.status != http.HTTPStatus.TOO_MANY_REQUESTS:
 791            return
 792
 793        if response.content_type != _APP_JSON:
 794            raise error.HTTPError(
 795                f"Being ratelimited on non JSON request, {response.content_type}.",
 796                http.HTTPStatus.TOO_MANY_REQUESTS,
 797            )
 798
 799        # The reason we have a type ignore here is that we guaranteed the content type is JSON above.
 800        json: typedefs.JSONObject = self._loads(await response.read())  # type: ignore
 801        retry_after = float(json.get("ThrottleSeconds", 15.0)) + 0.1
 802        max_calls: int = 0
 803
 804        while True:
 805            if max_calls == 10:
 806                # Max retries by default. We raise an error here.
 807                raise error.RateLimitedError(
 808                    body=json,
 809                    url=str(response.real_url),
 810                    retry_after=retry_after,
 811                )
 812
 813            # We sleep for a little bit to avoid funky behavior.
 814            _LOGGER.warning(
 815                "We're being ratelimited, Method %s Route %s. Sleeping for %.2fs.",
 816                method,
 817                route,
 818                retry_after,
 819            )
 820            await asyncio.sleep(retry_after)
 821            max_calls += 1
 822            continue
 823
 824    async def fetch_oauth2_tokens(self, code: str, /) -> builders.OAuth2Response:
 825        data = {
 826            "grant_type": "authorization_code",
 827            "code": code,
 828            "client_id": self._client_id,
 829            "client_secret": self._client_secret,
 830        }
 831
 832        response = await self._request(_POST, "", data=data, oauth2=True)
 833        assert isinstance(response, dict)
 834        return builders.OAuth2Response.build_response(response)
 835
 836    async def refresh_access_token(
 837        self, refresh_token: str, /
 838    ) -> builders.OAuth2Response:
 839        data = {
 840            "grant_type": "refresh_token",
 841            "refresh_token": refresh_token,
 842            "client_id": self._client_id,
 843            "client_secret": self._client_secret,
 844        }
 845
 846        response = await self._request(_POST, "", data=data, oauth2=True)
 847        assert isinstance(response, dict)
 848        return builders.OAuth2Response.build_response(response)
 849
 850    async def fetch_bungie_user(self, id: int) -> typedefs.JSONObject:
 851        resp = await self._request(_GET, f"User/GetBungieNetUserById/{id}/")
 852        assert isinstance(resp, dict)
 853        return resp
 854
 855    async def fetch_user_themes(self) -> typedefs.JSONArray:
 856        resp = await self._request(_GET, "User/GetAvailableThemes/")
 857        assert isinstance(resp, list)
 858        return resp
 859
 860    async def fetch_membership_from_id(
 861        self,
 862        id: int,
 863        type: enums.MembershipType | int = enums.MembershipType.NONE,
 864        /,
 865    ) -> typedefs.JSONObject:
 866        resp = await self._request(_GET, f"User/GetMembershipsById/{id}/{int(type)}")
 867        assert isinstance(resp, dict)
 868        return resp
 869
 870    async def fetch_membership(
 871        self,
 872        name: str,
 873        code: int,
 874        type: enums.MembershipType | int = enums.MembershipType.ALL,
 875        /,
 876    ) -> typedefs.JSONArray:
 877        resp = await self._request(
 878            _POST,
 879            f"Destiny2/SearchDestinyPlayerByBungieName/{int(type)}",
 880            json={"displayName": name, "displayNameCode": code},
 881        )
 882        assert isinstance(resp, list)
 883        return resp
 884
 885    async def fetch_sanitized_membership(
 886        self, membership_id: int, /
 887    ) -> typedefs.JSONObject:
 888        response = await self._request(
 889            _GET, f"User/GetSanitizedPlatformDisplayNames/{membership_id}/"
 890        )
 891        assert isinstance(response, dict)
 892        return response
 893
 894    async def search_users(self, name: str, /) -> typedefs.JSONObject:
 895        resp = await self._request(
 896            _POST,
 897            "User/Search/GlobalName/0",
 898            json={"displayNamePrefix": name},
 899        )
 900        assert isinstance(resp, dict)
 901        return resp
 902
 903    async def fetch_clan_from_id(
 904        self, id: int, /, access_token: str | None = None
 905    ) -> typedefs.JSONObject:
 906        resp = await self._request(_GET, f"GroupV2/{id}", auth=access_token)
 907        assert isinstance(resp, dict)
 908        return resp
 909
 910    async def fetch_clan(
 911        self,
 912        name: str,
 913        /,
 914        access_token: str | None = None,
 915        *,
 916        type: enums.GroupType | int = enums.GroupType.CLAN,
 917    ) -> typedefs.JSONObject:
 918        resp = await self._request(
 919            _GET, f"GroupV2/Name/{name}/{int(type)}", auth=access_token
 920        )
 921        assert isinstance(resp, dict)
 922        return resp
 923
 924    async def search_group(
 925        self,
 926        name: str,
 927        group_type: enums.GroupType | int = enums.GroupType.CLAN,
 928        *,
 929        creation_date: clans.GroupDate | int = 0,
 930        sort_by: int | None = None,
 931        group_member_count_filter: typing.Literal[0, 1, 2, 3] | None = None,
 932        locale_filter: str | None = None,
 933        tag_text: str | None = None,
 934        items_per_page: int | None = None,
 935        current_page: int | None = None,
 936        request_token: str | None = None,
 937    ) -> typedefs.JSONObject:
 938        payload: collections.MutableMapping[str, typing.Any] = {"name": name}
 939
 940        # as the official documentation says, you're not allowed to use those fields
 941        # on a clan search. it is safe to send the request with them being `null` but not filled with a value.
 942        if (
 943            group_type == enums.GroupType.CLAN
 944            and group_member_count_filter is not None
 945            and locale_filter
 946            and tag_text
 947        ):
 948            raise ValueError(
 949                "If you're searching for clans, (group_member_count_filter, locale_filter, tag_text) must be None."
 950            )
 951
 952        payload["groupType"] = int(group_type)
 953        payload["creationDate"] = int(creation_date)
 954        payload["sortBy"] = sort_by
 955        payload["groupMemberCount"] = group_member_count_filter
 956        payload["locale"] = locale_filter
 957        payload["tagText"] = tag_text
 958        payload["itemsPerPage"] = items_per_page
 959        payload["currentPage"] = current_page
 960        payload["requestToken"] = request_token
 961        payload["requestContinuationToken"] = request_token
 962
 963        resp = await self._request(_POST, "GroupV2/Search/", json=payload)
 964        assert isinstance(resp, dict)
 965        return resp
 966
 967    async def fetch_clan_admins(self, clan_id: int, /) -> typedefs.JSONObject:
 968        resp = await self._request(_GET, f"GroupV2/{clan_id}/AdminsAndFounder/")
 969        assert isinstance(resp, dict)
 970        return resp
 971
 972    async def fetch_clan_conversations(self, clan_id: int, /) -> typedefs.JSONArray:
 973        resp = await self._request(_GET, f"GroupV2/{clan_id}/OptionalConversations/")
 974        assert isinstance(resp, list)
 975        return resp
 976
 977    async def fetch_application(self, appid: int, /) -> typedefs.JSONObject:
 978        resp = await self._request(_GET, f"App/Application/{appid}")
 979        assert isinstance(resp, dict)
 980        return resp
 981
 982    async def fetch_character(
 983        self,
 984        member_id: int,
 985        membership_type: enums.MembershipType | int,
 986        character_id: int,
 987        components: collections.Sequence[enums.ComponentType],
 988        auth: str | None = None,
 989    ) -> typedefs.JSONObject:
 990        collector = _collect_components(components)
 991        response = await self._request(
 992            _GET,
 993            f"Destiny2/{int(membership_type)}/Profile/{member_id}/"
 994            f"Character/{character_id}/?components={collector}",
 995            auth=auth,
 996        )
 997        assert isinstance(response, dict)
 998        return response
 999
1000    async def fetch_activities(
1001        self,
1002        member_id: int,
1003        character_id: int,
1004        mode: enums.GameMode | int,
1005        membership_type: enums.MembershipType | int = enums.MembershipType.ALL,
1006        *,
1007        page: int = 0,
1008        limit: int = 1,
1009    ) -> typedefs.JSONObject:
1010        resp = await self._request(
1011            _GET,
1012            f"Destiny2/{int(membership_type)}/Account/"
1013            f"{member_id}/Character/{character_id}/Stats/Activities"
1014            f"/?mode={int(mode)}&count={limit}&page={page}",
1015        )
1016        assert isinstance(resp, dict)
1017        return resp
1018
1019    async def fetch_vendor_sales(self) -> typedefs.JSONObject:
1020        resp = await self._request(
1021            _GET,
1022            f"Destiny2/Vendors/?components={int(enums.ComponentType.VENDOR_SALES)}",
1023        )
1024        assert isinstance(resp, dict)
1025        return resp
1026
1027    async def fetch_profile(
1028        self,
1029        membership_id: int,
1030        type: enums.MembershipType | int,
1031        components: collections.Sequence[enums.ComponentType],
1032        auth: str | None = None,
1033    ) -> typedefs.JSONObject:
1034        collector = _collect_components(components)
1035        response = await self._request(
1036            _GET,
1037            f"Destiny2/{int(type)}/Profile/{membership_id}/?components={collector}",
1038            auth=auth,
1039        )
1040        assert isinstance(response, dict)
1041        return response
1042
1043    async def fetch_entity(self, type: str, hash: int) -> typedefs.JSONObject:
1044        response = await self._request(_GET, route=f"Destiny2/Manifest/{type}/{hash}")
1045        assert isinstance(response, dict)
1046        return response
1047
1048    async def fetch_inventory_item(self, hash: int, /) -> typedefs.JSONObject:
1049        resp = await self.fetch_entity("DestinyInventoryItemDefinition", hash)
1050        assert isinstance(resp, dict)
1051        return resp
1052
1053    async def fetch_objective_entity(self, hash: int, /) -> typedefs.JSONObject:
1054        resp = await self.fetch_entity("DestinyObjectiveDefinition", hash)
1055        assert isinstance(resp, dict)
1056        return resp
1057
1058    async def fetch_groups_for_member(
1059        self,
1060        member_id: int,
1061        member_type: enums.MembershipType | int,
1062        /,
1063        *,
1064        filter: int = 0,
1065        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1066    ) -> typedefs.JSONObject:
1067        resp = await self._request(
1068            _GET,
1069            f"GroupV2/User/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1070        )
1071        assert isinstance(resp, dict)
1072        return resp
1073
1074    async def fetch_potential_groups_for_member(
1075        self,
1076        member_id: int,
1077        member_type: enums.MembershipType | int,
1078        /,
1079        *,
1080        filter: int = 0,
1081        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1082    ) -> typedefs.JSONObject:
1083        resp = await self._request(
1084            _GET,
1085            f"GroupV2/User/Potential/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1086        )
1087        assert isinstance(resp, dict)
1088        return resp
1089
1090    async def fetch_clan_members(
1091        self,
1092        clan_id: int,
1093        /,
1094        *,
1095        name: str | None = None,
1096        type: enums.MembershipType | int = enums.MembershipType.NONE,
1097    ) -> typedefs.JSONObject:
1098        resp = await self._request(
1099            _GET,
1100            f"/GroupV2/{clan_id}/Members/?memberType={int(type)}&nameSearch={name if name else ''}&currentpage=1",
1101        )
1102        assert isinstance(resp, dict)
1103        return resp
1104
1105    async def fetch_hardlinked_credentials(
1106        self,
1107        credential: int,
1108        type: enums.CredentialType | int = enums.CredentialType.STEAMID,
1109        /,
1110    ) -> typedefs.JSONObject:
1111        resp = await self._request(
1112            _GET,
1113            f"User/GetMembershipFromHardLinkedCredential/{int(type)}/{credential}/",
1114        )
1115        assert isinstance(resp, dict)
1116        return resp
1117
1118    async def fetch_user_credentials(
1119        self, access_token: str, membership_id: int, /
1120    ) -> typedefs.JSONArray:
1121        resp = await self._request(
1122            _GET,
1123            f"User/GetCredentialTypesForTargetAccount/{membership_id}",
1124            auth=access_token,
1125        )
1126        assert isinstance(resp, list)
1127        return resp
1128
1129    async def insert_socket_plug(
1130        self,
1131        action_token: str,
1132        /,
1133        instance_id: int,
1134        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1135        character_id: int,
1136        membership_type: enums.MembershipType | int,
1137    ) -> typedefs.JSONObject:
1138        if isinstance(plug, builders.PlugSocketBuilder):
1139            plug = plug.collect()
1140
1141        body = {
1142            "actionToken": action_token,
1143            "itemInstanceId": instance_id,
1144            "plug": plug,
1145            "characterId": character_id,
1146            "membershipType": int(membership_type),
1147        }
1148        resp = await self._request(
1149            _POST, "Destiny2/Actions/Items/InsertSocketPlug", json=body
1150        )
1151        assert isinstance(resp, dict)
1152        return resp
1153
1154    async def insert_socket_plug_free(
1155        self,
1156        access_token: str,
1157        /,
1158        instance_id: int,
1159        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1160        character_id: int,
1161        membership_type: enums.MembershipType | int,
1162    ) -> typedefs.JSONObject:
1163        if isinstance(plug, builders.PlugSocketBuilder):
1164            plug = plug.collect()
1165
1166        body = {
1167            "itemInstanceId": instance_id,
1168            "plug": plug,
1169            "characterId": character_id,
1170            "membershipType": int(membership_type),
1171        }
1172        resp = await self._request(
1173            _POST,
1174            "Destiny2/Actions/Items/InsertSocketPlugFree",
1175            json=body,
1176            auth=access_token,
1177        )
1178        assert isinstance(resp, dict)
1179        return resp
1180
1181    @helpers.unstable
1182    async def set_item_lock_state(
1183        self,
1184        access_token: str,
1185        state: bool,
1186        /,
1187        item_id: int,
1188        character_id: int,
1189        membership_type: enums.MembershipType | int,
1190    ) -> int:
1191        body = {
1192            "state": state,
1193            "itemId": item_id,
1194            "characterId": character_id,
1195            "membershipType": int(membership_type),
1196        }
1197        response = await self._request(
1198            _POST,
1199            "Destiny2/Actions/Items/SetLockState",
1200            json=body,
1201            auth=access_token,
1202        )
1203        assert isinstance(response, int)
1204        return response
1205
1206    async def set_quest_track_state(
1207        self,
1208        access_token: str,
1209        state: bool,
1210        /,
1211        item_id: int,
1212        character_id: int,
1213        membership_type: enums.MembershipType | int,
1214    ) -> int:
1215        body = {
1216            "state": state,
1217            "itemId": item_id,
1218            "characterId": character_id,
1219            "membership_type": int(membership_type),
1220        }
1221        response = await self._request(
1222            _POST,
1223            "Destiny2/Actions/Items/SetTrackedState",
1224            json=body,
1225            auth=access_token,
1226        )
1227        assert isinstance(response, int)
1228        return response
1229
1230    async def fetch_manifest_path(self) -> typedefs.JSONObject:
1231        path = await self._request(_GET, "Destiny2/Manifest")
1232        assert isinstance(path, dict)
1233        return path
1234
1235    async def read_manifest_bytes(self, language: _ALLOWED_LANGS = "en", /) -> bytes:
1236        _ensure_manifest_language(language)
1237
1238        content = await self.fetch_manifest_path()
1239        resp = await self._request(
1240            _GET,
1241            content["mobileWorldContentPaths"][language],
1242            unwrap_bytes=True,
1243            base=True,
1244        )
1245        assert isinstance(resp, bytes)
1246        return resp
1247
1248    async def download_sqlite_manifest(
1249        self,
1250        language: _ALLOWED_LANGS = "en",
1251        name: str = "manifest",
1252        path: pathlib.Path | str = ".",
1253        *,
1254        force: bool = False,
1255        executor: concurrent.futures.Executor | None = None,
1256    ) -> pathlib.Path:
1257        complete_path = _get_path(name, path, sql=True)
1258
1259        if complete_path.exists():
1260            if force:
1261                _LOGGER.info(
1262                    f"Found manifest in {complete_path!s}. Forcing to Re-Download."
1263                )
1264                complete_path.unlink(missing_ok=True)
1265
1266                return await self.download_sqlite_manifest(
1267                    language, name, path, force=force
1268                )
1269
1270            else:
1271                raise FileExistsError(
1272                    "Manifest file already exists, "
1273                    "To force download, set the `force` parameter to `True`."
1274                )
1275
1276        _LOGGER.info(f"Downloading manifest. Location: {complete_path!s}")
1277        data_bytes = await self.read_manifest_bytes(language)
1278        await asyncio.get_running_loop().run_in_executor(
1279            executor, _write_sqlite_bytes, data_bytes, path, name
1280        )
1281        _LOGGER.info("Finished downloading manifest.")
1282        return _get_path(name, path, sql=True)
1283
1284    async def download_json_manifest(
1285        self,
1286        file_name: str = "manifest",
1287        path: str | pathlib.Path = ".",
1288        *,
1289        language: _ALLOWED_LANGS = "en",
1290        executor: concurrent.futures.Executor | None = None,
1291    ) -> pathlib.Path:
1292        _ensure_manifest_language(language)
1293        full_path = _get_path(file_name, path)
1294        _LOGGER.info(f"Downloading manifest JSON to {full_path!r}...")
1295
1296        content = await self.fetch_manifest_path()
1297        json_bytes = await self._request(
1298            _GET,
1299            content["jsonWorldContentPaths"][language],
1300            unwrap_bytes=True,
1301            base=True,
1302        )
1303
1304        assert isinstance(json_bytes, bytes)
1305        await asyncio.get_running_loop().run_in_executor(
1306            executor, _write_json_bytes, json_bytes, file_name, path
1307        )
1308        _LOGGER.info("Finished downloading manifest JSON.")
1309        return full_path
1310
1311    async def fetch_manifest_version(self) -> str:
1312        # This is guaranteed str.
1313        return (await self.fetch_manifest_path())["version"]
1314
1315    async def fetch_linked_profiles(
1316        self,
1317        member_id: int,
1318        member_type: enums.MembershipType | int,
1319        /,
1320        *,
1321        all: bool = False,
1322    ) -> typedefs.JSONObject:
1323        resp = await self._request(
1324            _GET,
1325            f"Destiny2/{int(member_type)}/Profile/{member_id}/LinkedProfiles/?getAllMemberships={all}",
1326        )
1327        assert isinstance(resp, dict)
1328        return resp
1329
1330    async def fetch_clan_banners(self) -> typedefs.JSONObject:
1331        resp = await self._request(_GET, "Destiny2/Clan/ClanBannerDictionary/")
1332        assert isinstance(resp, dict)
1333        return resp
1334
1335    async def fetch_public_milestones(self) -> typedefs.JSONObject:
1336        resp = await self._request(_GET, "Destiny2/Milestones/")
1337        assert isinstance(resp, dict)
1338        return resp
1339
1340    async def fetch_public_milestone_content(
1341        self, milestone_hash: int, /
1342    ) -> typedefs.JSONObject:
1343        resp = await self._request(
1344            _GET, f"Destiny2/Milestones/{milestone_hash}/Content/"
1345        )
1346        assert isinstance(resp, dict)
1347        return resp
1348
1349    async def fetch_current_user_memberships(
1350        self, access_token: str, /
1351    ) -> typedefs.JSONObject:
1352        resp = await self._request(
1353            _GET,
1354            "User/GetMembershipsForCurrentUser/",
1355            auth=access_token,
1356        )
1357        assert isinstance(resp, dict)
1358        return resp
1359
1360    async def equip_item(
1361        self,
1362        access_token: str,
1363        /,
1364        item_id: int,
1365        character_id: int,
1366        membership_type: enums.MembershipType | int,
1367    ) -> None:
1368        payload = {
1369            "itemId": item_id,
1370            "characterId": character_id,
1371            "membershipType": int(membership_type),
1372        }
1373
1374        await self._request(
1375            _POST,
1376            "Destiny2/Actions/Items/EquipItem/",
1377            json=payload,
1378            auth=access_token,
1379        )
1380
1381    async def equip_items(
1382        self,
1383        access_token: str,
1384        /,
1385        item_ids: collections.Sequence[int],
1386        character_id: int,
1387        membership_type: enums.MembershipType | int,
1388    ) -> None:
1389        payload = {
1390            "itemIds": item_ids,
1391            "characterId": character_id,
1392            "membershipType": int(membership_type),
1393        }
1394        await self._request(
1395            _POST,
1396            "Destiny2/Actions/Items/EquipItems/",
1397            json=payload,
1398            auth=access_token,
1399        )
1400
1401    async def ban_clan_member(
1402        self,
1403        access_token: str,
1404        /,
1405        group_id: int,
1406        membership_id: int,
1407        membership_type: enums.MembershipType | int,
1408        *,
1409        length: int = 0,
1410        comment: str | None = None,
1411    ) -> None:
1412        payload = {"comment": str(comment), "length": length}
1413        await self._request(
1414            _POST,
1415            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Ban/",
1416            json=payload,
1417            auth=access_token,
1418        )
1419
1420    async def unban_clan_member(
1421        self,
1422        access_token: str,
1423        /,
1424        group_id: int,
1425        membership_id: int,
1426        membership_type: enums.MembershipType | int,
1427    ) -> None:
1428        await self._request(
1429            _POST,
1430            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Unban/",
1431            auth=access_token,
1432        )
1433
1434    async def kick_clan_member(
1435        self,
1436        access_token: str,
1437        /,
1438        group_id: int,
1439        membership_id: int,
1440        membership_type: enums.MembershipType | int,
1441    ) -> typedefs.JSONObject:
1442        resp = await self._request(
1443            _POST,
1444            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Kick/",
1445            auth=access_token,
1446        )
1447        assert isinstance(resp, dict)
1448        return resp
1449
1450    async def edit_clan(
1451        self,
1452        access_token: str,
1453        /,
1454        group_id: int,
1455        *,
1456        name: str | None = None,
1457        about: str | None = None,
1458        motto: str | None = None,
1459        theme: str | None = None,
1460        tags: collections.Sequence[str] | None = None,
1461        is_public: bool | None = None,
1462        locale: str | None = None,
1463        avatar_image_index: int | None = None,
1464        membership_option: enums.MembershipOption | int | None = None,
1465        allow_chat: bool | None = None,
1466        chat_security: typing.Literal[0, 1] | None = None,
1467        call_sign: str | None = None,
1468        homepage: typing.Literal[0, 1, 2] | None = None,
1469        enable_invite_messaging_for_admins: bool | None = None,
1470        default_publicity: typing.Literal[0, 1, 2] | None = None,
1471        is_public_topic_admin: bool | None = None,
1472    ) -> None:
1473        payload = {
1474            "name": name,
1475            "about": about,
1476            "motto": motto,
1477            "theme": theme,
1478            "tags": tags,
1479            "isPublic": is_public,
1480            "avatarImageIndex": avatar_image_index,
1481            "isPublicTopicAdminOnly": is_public_topic_admin,
1482            "allowChat": allow_chat,
1483            "chatSecurity": chat_security,
1484            "callsign": call_sign,
1485            "homepage": homepage,
1486            "enableInvitationMessagingForAdmins": enable_invite_messaging_for_admins,
1487            "defaultPublicity": default_publicity,
1488            "locale": locale,
1489        }
1490        if membership_option is not None:
1491            payload["membershipOption"] = int(membership_option)
1492
1493        await self._request(
1494            _POST,
1495            f"GroupV2/{group_id}/Edit",
1496            json=payload,
1497            auth=access_token,
1498        )
1499
1500    async def edit_clan_options(
1501        self,
1502        access_token: str,
1503        /,
1504        group_id: int,
1505        *,
1506        invite_permissions_override: bool | None = None,
1507        update_culture_permissionOverride: bool | None = None,
1508        host_guided_game_permission_override: typing.Literal[0, 1, 2] | None = None,
1509        update_banner_permission_override: bool | None = None,
1510        join_level: enums.ClanMemberType | int | None = None,
1511    ) -> None:
1512        payload = {
1513            "InvitePermissionOverride": invite_permissions_override,
1514            "UpdateCulturePermissionOverride": update_culture_permissionOverride,
1515            "HostGuidedGamePermissionOverride": host_guided_game_permission_override,
1516            "UpdateBannerPermissionOverride": update_banner_permission_override,
1517            "JoinLevel": int(join_level) if join_level else None,
1518        }
1519
1520        await self._request(
1521            _POST,
1522            f"GroupV2/{group_id}/EditFounderOptions",
1523            json=payload,
1524            auth=access_token,
1525        )
1526
1527    async def report_player(
1528        self,
1529        access_token: str,
1530        /,
1531        activity_id: int,
1532        character_id: int,
1533        reason_hashes: collections.Sequence[int],
1534        reason_category_hashes: collections.Sequence[int],
1535    ) -> None:
1536        await self._request(
1537            _POST,
1538            f"Destiny2/Stats/PostGameCarnageReport/{activity_id}/Report/",
1539            json={
1540                "reasonCategoryHashes": reason_category_hashes,
1541                "reasonHashes": reason_hashes,
1542                "offendingCharacterId": character_id,
1543            },
1544            auth=access_token,
1545        )
1546
1547    async def fetch_friends(self, access_token: str, /) -> typedefs.JSONObject:
1548        resp = await self._request(
1549            _GET,
1550            "Social/Friends/",
1551            auth=access_token,
1552        )
1553        assert isinstance(resp, dict)
1554        return resp
1555
1556    async def fetch_friend_requests(self, access_token: str, /) -> typedefs.JSONObject:
1557        resp = await self._request(
1558            _GET,
1559            "Social/Friends/Requests",
1560            auth=access_token,
1561        )
1562        assert isinstance(resp, dict)
1563        return resp
1564
1565    async def accept_friend_request(self, access_token: str, /, member_id: int) -> None:
1566        await self._request(
1567            _POST,
1568            f"Social/Friends/Requests/Accept/{member_id}",
1569            auth=access_token,
1570        )
1571
1572    async def send_friend_request(self, access_token: str, /, member_id: int) -> None:
1573        await self._request(
1574            _POST,
1575            f"Social/Friends/Add/{member_id}",
1576            auth=access_token,
1577        )
1578
1579    async def decline_friend_request(
1580        self, access_token: str, /, member_id: int
1581    ) -> None:
1582        await self._request(
1583            _POST,
1584            f"Social/Friends/Requests/Decline/{member_id}",
1585            auth=access_token,
1586        )
1587
1588    async def remove_friend(self, access_token: str, /, member_id: int) -> None:
1589        await self._request(
1590            _POST,
1591            f"Social/Friends/Remove/{member_id}",
1592            auth=access_token,
1593        )
1594
1595    async def remove_friend_request(self, access_token: str, /, member_id: int) -> None:
1596        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1597        await self._request(
1598            _POST,
1599            f"Social/Friends/Requests/Remove/{member_id}",
1600            auth=access_token,
1601        )
1602
1603    async def approve_all_pending_group_users(
1604        self,
1605        access_token: str,
1606        /,
1607        group_id: int,
1608        message: str | None = None,
1609    ) -> None:
1610        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1611        await self._request(
1612            _POST,
1613            f"GroupV2/{group_id}/Members/ApproveAll",
1614            auth=access_token,
1615            json={"message": str(message)},
1616        )
1617
1618    async def deny_all_pending_group_users(
1619        self,
1620        access_token: str,
1621        /,
1622        group_id: int,
1623        *,
1624        message: str | None = None,
1625    ) -> None:
1626        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1627        await self._request(
1628            _POST,
1629            f"GroupV2/{group_id}/Members/DenyAll",
1630            auth=access_token,
1631            json={"message": str(message)},
1632        )
1633
1634    async def add_optional_conversation(
1635        self,
1636        access_token: str,
1637        /,
1638        group_id: int,
1639        *,
1640        name: str | None = None,
1641        security: typing.Literal[0, 1] = 0,
1642    ) -> None:
1643        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1644        payload = {"chatName": str(name), "chatSecurity": security}
1645        await self._request(
1646            _POST,
1647            f"GroupV2/{group_id}/OptionalConversations/Add",
1648            json=payload,
1649            auth=access_token,
1650        )
1651
1652    async def edit_optional_conversation(
1653        self,
1654        access_token: str,
1655        /,
1656        group_id: int,
1657        conversation_id: int,
1658        *,
1659        name: str | None = None,
1660        security: typing.Literal[0, 1] = 0,
1661        enable_chat: bool = False,
1662    ) -> None:
1663        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1664        payload = {
1665            "chatEnabled": enable_chat,
1666            "chatName": str(name),
1667            "chatSecurity": security,
1668        }
1669        await self._request(
1670            _POST,
1671            f"GroupV2/{group_id}/OptionalConversations/Edit/{conversation_id}",
1672            json=payload,
1673            auth=access_token,
1674        )
1675
1676    async def transfer_item(
1677        self,
1678        access_token: str,
1679        /,
1680        item_id: int,
1681        item_hash: int,
1682        character_id: int,
1683        member_type: enums.MembershipType | int,
1684        *,
1685        stack_size: int = 1,
1686        vault: bool = False,
1687    ) -> None:
1688        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1689        payload = {
1690            "characterId": character_id,
1691            "membershipType": int(member_type),
1692            "itemId": item_id,
1693            "itemReferenceHash": item_hash,
1694            "stackSize": stack_size,
1695            "transferToVault": vault,
1696        }
1697        await self._request(
1698            _POST,
1699            "Destiny2/Actions/Items/TransferItem",
1700            json=payload,
1701            auth=access_token,
1702        )
1703
1704    async def pull_item(
1705        self,
1706        access_token: str,
1707        /,
1708        item_id: int,
1709        item_hash: int,
1710        character_id: int,
1711        member_type: enums.MembershipType | int,
1712        *,
1713        stack_size: int = 1,
1714        vault: bool = False,
1715    ) -> None:
1716        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1717        payload = {
1718            "characterId": character_id,
1719            "membershipType": int(member_type),
1720            "itemId": item_id,
1721            "itemReferenceHash": item_hash,
1722            "stackSize": stack_size,
1723        }
1724        await self._request(
1725            _POST,
1726            "Destiny2/Actions/Items/PullFromPostmaster",
1727            json=payload,
1728            auth=access_token,
1729        )
1730        if vault:
1731            await self.transfer_item(
1732                access_token,
1733                item_id=item_id,
1734                item_hash=item_hash,
1735                character_id=character_id,
1736                member_type=member_type,
1737                stack_size=stack_size,
1738                vault=True,
1739            )
1740
1741    async def fetch_fireteams(
1742        self,
1743        activity_type: fireteams.FireteamActivity | int,
1744        *,
1745        platform: fireteams.FireteamPlatform | int = fireteams.FireteamPlatform.ANY,
1746        language: fireteams.FireteamLanguage | str = fireteams.FireteamLanguage.ALL,
1747        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1748        page: int = 0,
1749        slots_filter: int = 0,
1750    ) -> typedefs.JSONObject:
1751        resp = await self._request(
1752            _GET,
1753            f"Fireteam/Search/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{page}/?langFilter={str(language)}",  # noqa: E501 Line too long
1754        )
1755        assert isinstance(resp, dict)
1756        return resp
1757
1758    async def fetch_available_clan_fireteams(
1759        self,
1760        access_token: str,
1761        group_id: int,
1762        activity_type: fireteams.FireteamActivity | int,
1763        *,
1764        platform: fireteams.FireteamPlatform | int,
1765        language: fireteams.FireteamLanguage | str,
1766        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1767        page: int = 0,
1768        public_only: bool = False,
1769        slots_filter: int = 0,
1770    ) -> typedefs.JSONObject:
1771        resp = await self._request(
1772            _GET,
1773            f"Fireteam/Clan/{group_id}/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{public_only}/{page}",  # noqa: E501
1774            json={"langFilter": str(language)},
1775            auth=access_token,
1776        )
1777        assert isinstance(resp, dict)
1778        return resp
1779
1780    async def fetch_clan_fireteam(
1781        self, access_token: str, fireteam_id: int, group_id: int
1782    ) -> typedefs.JSONObject:
1783        resp = await self._request(
1784            _GET,
1785            f"Fireteam/Clan/{group_id}/Summary/{fireteam_id}",
1786            auth=access_token,
1787        )
1788        assert isinstance(resp, dict)
1789        return resp
1790
1791    async def fetch_my_clan_fireteams(
1792        self,
1793        access_token: str,
1794        group_id: int,
1795        *,
1796        include_closed: bool = True,
1797        platform: fireteams.FireteamPlatform | int,
1798        language: fireteams.FireteamLanguage | str,
1799        filtered: bool = True,
1800        page: int = 0,
1801    ) -> typedefs.JSONObject:
1802        payload = {"groupFilter": filtered, "langFilter": str(language)}
1803
1804        resp = await self._request(
1805            _GET,
1806            f"Fireteam/Clan/{group_id}/My/{int(platform)}/{include_closed}/{page}",
1807            json=payload,
1808            auth=access_token,
1809        )
1810        assert isinstance(resp, dict)
1811        return resp
1812
1813    async def fetch_private_clan_fireteams(
1814        self, access_token: str, group_id: int, /
1815    ) -> int:
1816        resp = await self._request(
1817            _GET,
1818            f"Fireteam/Clan/{group_id}/ActiveCount",
1819            auth=access_token,
1820        )
1821        assert isinstance(resp, int)
1822        return resp
1823
1824    async def fetch_post_activity(self, instance_id: int, /) -> typedefs.JSONObject:
1825        resp = await self._request(
1826            _GET, f"Destiny2/Stats/PostGameCarnageReport/{instance_id}"
1827        )
1828        assert isinstance(resp, dict)
1829        return resp
1830
1831    @helpers.unstable
1832    async def search_entities(
1833        self, name: str, entity_type: str, *, page: int = 0
1834    ) -> typedefs.JSONObject:
1835        resp = await self._request(
1836            _GET,
1837            f"Destiny2/Armory/Search/{entity_type}/{name}/",
1838            json={"page": page},
1839        )
1840        assert isinstance(resp, dict)
1841        return resp
1842
1843    async def fetch_unique_weapon_history(
1844        self,
1845        membership_id: int,
1846        character_id: int,
1847        membership_type: enums.MembershipType | int,
1848    ) -> typedefs.JSONObject:
1849        resp = await self._request(
1850            _GET,
1851            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/UniqueWeapons/",
1852        )
1853        assert isinstance(resp, dict)
1854        return resp
1855
1856    async def fetch_item(
1857        self,
1858        member_id: int,
1859        item_id: int,
1860        membership_type: enums.MembershipType | int,
1861        components: collections.Sequence[enums.ComponentType],
1862    ) -> typedefs.JSONObject:
1863        collector = _collect_components(components)
1864
1865        resp = await self._request(
1866            _GET,
1867            f"Destiny2/{int(membership_type)}/Profile/{member_id}/Item/{item_id}/?components={collector}",
1868        )
1869        assert isinstance(resp, dict)
1870        return resp
1871
1872    async def fetch_clan_weekly_rewards(self, clan_id: int, /) -> typedefs.JSONObject:
1873        resp = await self._request(_GET, f"Destiny2/Clan/{clan_id}/WeeklyRewardState/")
1874        assert isinstance(resp, dict)
1875        return resp
1876
1877    async def fetch_available_locales(self) -> typedefs.JSONObject:
1878        resp = await self._request(_GET, "Destiny2/Manifest/DestinyLocaleDefinition/")
1879        assert isinstance(resp, dict)
1880        return resp
1881
1882    async def fetch_common_settings(self) -> typedefs.JSONObject:
1883        resp = await self._request(_GET, "Settings")
1884        assert isinstance(resp, dict)
1885        return resp
1886
1887    async def fetch_user_systems_overrides(self) -> typedefs.JSONObject:
1888        resp = await self._request(_GET, "UserSystemOverrides")
1889        assert isinstance(resp, dict)
1890        return resp
1891
1892    async def fetch_global_alerts(
1893        self, *, include_streaming: bool = False
1894    ) -> typedefs.JSONArray:
1895        resp = await self._request(
1896            _GET, f"GlobalAlerts/?includestreaming={include_streaming}"
1897        )
1898        assert isinstance(resp, list)
1899        return resp
1900
1901    async def awainitialize_request(
1902        self,
1903        access_token: str,
1904        type: typing.Literal[0, 1],
1905        membership_type: enums.MembershipType | int,
1906        /,
1907        *,
1908        affected_item_id: int | None = None,
1909        character_id: int | None = None,
1910    ) -> typedefs.JSONObject:
1911        body = {"type": type, "membershipType": int(membership_type)}
1912
1913        if affected_item_id is not None:
1914            body["affectedItemId"] = affected_item_id
1915
1916        if character_id is not None:
1917            body["characterId"] = character_id
1918
1919        resp = await self._request(
1920            _POST, "Destiny2/Awa/Initialize", json=body, auth=access_token
1921        )
1922        assert isinstance(resp, dict)
1923        return resp
1924
1925    async def awaget_action_token(
1926        self, access_token: str, correlation_id: str, /
1927    ) -> typedefs.JSONObject:
1928        resp = await self._request(
1929            _POST,
1930            f"Destiny2/Awa/GetActionToken/{correlation_id}",
1931            auth=access_token,
1932        )
1933        assert isinstance(resp, dict)
1934        return resp
1935
1936    async def awa_provide_authorization_result(
1937        self,
1938        access_token: str,
1939        selection: int,
1940        correlation_id: str,
1941        nonce: collections.MutableSequence[str | bytes],
1942    ) -> int:
1943        body = {"selection": selection, "correlationId": correlation_id, "nonce": nonce}
1944
1945        resp = await self._request(
1946            _POST,
1947            "Destiny2/Awa/AwaProvideAuthorizationResult",
1948            json=body,
1949            auth=access_token,
1950        )
1951        assert isinstance(resp, int)
1952        return resp
1953
1954    async def fetch_vendors(
1955        self,
1956        access_token: str,
1957        character_id: int,
1958        membership_id: int,
1959        membership_type: enums.MembershipType | int,
1960        /,
1961        components: collections.Sequence[enums.ComponentType],
1962        filter: int | None = None,
1963    ) -> typedefs.JSONObject:
1964        components_ = _collect_components(components)
1965        route = (
1966            f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1967            f"/Character/{character_id}/Vendors/?components={components_}"
1968        )
1969
1970        if filter is not None:
1971            route = route + f"&filter={filter}"
1972
1973        resp = await self._request(
1974            _GET,
1975            route,
1976            auth=access_token,
1977        )
1978        assert isinstance(resp, dict)
1979        return resp
1980
1981    async def fetch_vendor(
1982        self,
1983        access_token: str,
1984        character_id: int,
1985        membership_id: int,
1986        membership_type: enums.MembershipType | int,
1987        vendor_hash: int,
1988        /,
1989        components: collections.Sequence[enums.ComponentType],
1990    ) -> typedefs.JSONObject:
1991        components_ = _collect_components(components)
1992        resp = await self._request(
1993            _GET,
1994            (
1995                f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1996                f"/Character/{character_id}/Vendors/{vendor_hash}/?components={components_}"
1997            ),
1998            auth=access_token,
1999        )
2000        assert isinstance(resp, dict)
2001        return resp
2002
2003    async def fetch_application_api_usage(
2004        self,
2005        access_token: str,
2006        application_id: int,
2007        /,
2008        *,
2009        start: datetime.datetime | None = None,
2010        end: datetime.datetime | None = None,
2011    ) -> typedefs.JSONObject:
2012        end_date, start_date = time.parse_date_range(end, start)
2013        resp = await self._request(
2014            _GET,
2015            f"App/ApiUsage/{application_id}/?end={end_date}&start={start_date}",
2016            auth=access_token,
2017        )
2018        assert isinstance(resp, dict)
2019        return resp
2020
2021    async def fetch_bungie_applications(self) -> typedefs.JSONArray:
2022        resp = await self._request(_GET, "App/FirstParty")
2023        assert isinstance(resp, list)
2024        return resp
2025
2026    async def fetch_content_type(self, type: str, /) -> typedefs.JSONObject:
2027        resp = await self._request(_GET, f"Content/GetContentType/{type}/")
2028        assert isinstance(resp, dict)
2029        return resp
2030
2031    async def fetch_content_by_id(
2032        self, id: int, locale: str, /, *, head: bool = False
2033    ) -> typedefs.JSONObject:
2034        resp = await self._request(
2035            _GET,
2036            f"Content/GetContentById/{id}/{locale}/",
2037            json={"head": head},
2038        )
2039        assert isinstance(resp, dict)
2040        return resp
2041
2042    async def fetch_content_by_tag_and_type(
2043        self, locale: str, tag: str, type: str, *, head: bool = False
2044    ) -> typedefs.JSONObject:
2045        resp = await self._request(
2046            _GET,
2047            f"Content/GetContentByTagAndType/{tag}/{type}/{locale}/",
2048            json={"head": head},
2049        )
2050        assert isinstance(resp, dict)
2051        return resp
2052
2053    async def search_content_with_text(
2054        self,
2055        locale: str,
2056        /,
2057        content_type: str,
2058        search_text: str,
2059        tag: str,
2060        *,
2061        page: int | None = None,
2062        source: str | None = None,
2063    ) -> typedefs.JSONObject:
2064        body: typedefs.JSONObject = {
2065            "locale": locale,
2066            "currentpage": page or 1,
2067            "ctype": content_type,
2068            "searchtxt": search_text,
2069            "searchtext": search_text,
2070            "tag": tag,
2071            "source": source,
2072        }
2073
2074        resp = await self._request(_GET, "Content/Search", params=body)
2075        assert isinstance(resp, dict)
2076        return resp
2077
2078    async def search_content_by_tag_and_type(
2079        self,
2080        locale: str,
2081        tag: str,
2082        type: str,
2083        *,
2084        page: int | None = None,
2085    ) -> typedefs.JSONObject:
2086        body: typedefs.JSONObject = {"currentpage": page or 1}
2087
2088        resp = await self._request(
2089            _GET,
2090            f"Content/SearchContentByTagAndType/{tag}/{type}/{locale}/",
2091            params=body,
2092        )
2093        assert isinstance(resp, dict)
2094        return resp
2095
2096    async def search_help_articles(
2097        self, text: str, size: str, /
2098    ) -> typedefs.JSONObject:
2099        resp = await self._request(_GET, f"Content/SearchHelpArticles/{text}/{size}/")
2100        assert isinstance(resp, dict)
2101        return resp
2102
2103    async def fetch_topics_page(
2104        self,
2105        category_filter: int,
2106        group: int,
2107        date_filter: int,
2108        sort: str | bytes,
2109        *,
2110        page: int | None = None,
2111        locales: collections.Iterable[str] | None = None,
2112        tag_filter: str | None = None,
2113    ) -> typedefs.JSONObject:
2114        params = {
2115            "locales": ",".join(locales) if locales is not None else "en",
2116        }
2117        if tag_filter:
2118            params["tagstring"] = tag_filter
2119
2120        resp = await self._request(
2121            _GET,
2122            f"Forum/GetTopicsPaged/{page or 0}/0/{group}/{sort!s}/{date_filter}/{category_filter}/",
2123            params=params,
2124        )
2125        assert isinstance(resp, dict)
2126        return resp
2127
2128    async def fetch_core_topics_page(
2129        self,
2130        category_filter: int,
2131        date_filter: int,
2132        sort: str | bytes,
2133        *,
2134        page: int | None = None,
2135        locales: collections.Iterable[str] | None = None,
2136    ) -> typedefs.JSONObject:
2137        resp = await self._request(
2138            _GET,
2139            f"Forum/GetCoreTopicsPaged/{page or 0}"
2140            f"/{sort!s}/{date_filter}/{category_filter}/?locales={','.join(locales) if locales else 'en'}",
2141        )
2142        assert isinstance(resp, dict)
2143        return resp
2144
2145    async def fetch_posts_threaded_page(
2146        self,
2147        parent_post: bool,
2148        page: int,
2149        page_size: int,
2150        parent_post_id: int,
2151        reply_size: int,
2152        root_thread_mode: bool,
2153        sort_mode: int,
2154        show_banned: str | None = None,
2155    ) -> typedefs.JSONObject:
2156        resp = await self._request(
2157            _GET,
2158            f"Forum/GetPostsThreadedPaged/{parent_post}/{page}/"
2159            f"{page_size}/{reply_size}/{parent_post_id}/{root_thread_mode}/{sort_mode}/",
2160            json={"showbanned": show_banned},
2161        )
2162        assert isinstance(resp, dict)
2163        return resp
2164
2165    async def fetch_posts_threaded_page_from_child(
2166        self,
2167        child_id: bool,
2168        page: int,
2169        page_size: int,
2170        reply_size: int,
2171        root_thread_mode: bool,
2172        sort_mode: int,
2173        show_banned: str | None = None,
2174    ) -> typedefs.JSONObject:
2175        resp = await self._request(
2176            _GET,
2177            f"Forum/GetPostsThreadedPagedFromChild/{child_id}/"
2178            f"{page}/{page_size}/{reply_size}/{root_thread_mode}/{sort_mode}/",
2179            json={"showbanned": show_banned},
2180        )
2181        assert isinstance(resp, dict)
2182        return resp
2183
2184    async def fetch_post_and_parent(
2185        self, child_id: int, /, *, show_banned: str | None = None
2186    ) -> typedefs.JSONObject:
2187        resp = await self._request(
2188            _GET,
2189            f"Forum/GetPostAndParent/{child_id}/",
2190            json={"showbanned": show_banned},
2191        )
2192        assert isinstance(resp, dict)
2193        return resp
2194
2195    async def fetch_posts_and_parent_awaiting(
2196        self, child_id: int, /, *, show_banned: str | None = None
2197    ) -> typedefs.JSONObject:
2198        resp = await self._request(
2199            _GET,
2200            f"Forum/GetPostAndParentAwaitingApproval/{child_id}/",
2201            json={"showbanned": show_banned},
2202        )
2203        assert isinstance(resp, dict)
2204        return resp
2205
2206    async def fetch_topic_for_content(self, content_id: int, /) -> int:
2207        resp = await self._request(_GET, f"Forum/GetTopicForContent/{content_id}/")
2208        assert isinstance(resp, int)
2209        return resp
2210
2211    async def fetch_forum_tag_suggestions(
2212        self, partial_tag: str, /
2213    ) -> typedefs.JSONObject:
2214        resp = await self._request(
2215            _GET,
2216            "Forum/GetForumTagSuggestions/",
2217            json={"partialtag": partial_tag},
2218        )
2219        assert isinstance(resp, dict)
2220        return resp
2221
2222    async def fetch_poll(self, topic_id: int, /) -> typedefs.JSONObject:
2223        resp = await self._request(_GET, f"Forum/Poll/{topic_id}/")
2224        assert isinstance(resp, dict)
2225        return resp
2226
2227    async def fetch_recruitment_thread_summaries(self) -> typedefs.JSONArray:
2228        resp = await self._request(_POST, "Forum/Recruit/Summaries/")
2229        assert isinstance(resp, list)
2230        return resp
2231
2232    async def fetch_recommended_groups(
2233        self,
2234        access_token: str,
2235        /,
2236        *,
2237        date_range: int = 0,
2238        group_type: enums.GroupType | int = enums.GroupType.CLAN,
2239    ) -> typedefs.JSONArray:
2240        resp = await self._request(
2241            _POST,
2242            f"GroupV2/Recommended/{int(group_type)}/{date_range}/",
2243            auth=access_token,
2244        )
2245        assert isinstance(resp, list)
2246        return resp
2247
2248    async def fetch_available_avatars(self) -> collections.Mapping[str, int]:
2249        resp = await self._request(_GET, "GroupV2/GetAvailableAvatars/")
2250        assert isinstance(resp, dict)
2251        return resp
2252
2253    async def fetch_user_clan_invite_setting(
2254        self,
2255        access_token: str,
2256        /,
2257        membership_type: enums.MembershipType | int,
2258    ) -> bool:
2259        resp = await self._request(
2260            _GET,
2261            f"GroupV2/GetUserClanInviteSetting/{int(membership_type)}/",
2262            auth=access_token,
2263        )
2264        assert isinstance(resp, bool)
2265        return resp
2266
2267    async def fetch_banned_group_members(
2268        self, access_token: str, group_id: int, /, *, page: int = 1
2269    ) -> typedefs.JSONObject:
2270        resp = await self._request(
2271            _GET,
2272            f"GroupV2/{group_id}/Banned/?currentpage={page}",
2273            auth=access_token,
2274        )
2275        assert isinstance(resp, dict)
2276        return resp
2277
2278    async def fetch_pending_group_memberships(
2279        self, access_token: str, group_id: int, /, *, current_page: int = 1
2280    ) -> typedefs.JSONObject:
2281        resp = await self._request(
2282            _GET,
2283            f"GroupV2/{group_id}/Members/Pending/?currentpage={current_page}",
2284            auth=access_token,
2285        )
2286        assert isinstance(resp, dict)
2287        return resp
2288
2289    async def fetch_invited_group_memberships(
2290        self, access_token: str, group_id: int, /, *, current_page: int = 1
2291    ) -> typedefs.JSONObject:
2292        resp = await self._request(
2293            _GET,
2294            f"GroupV2/{group_id}/Members/InvitedIndividuals/?currentpage={current_page}",
2295            auth=access_token,
2296        )
2297        assert isinstance(resp, dict)
2298        return resp
2299
2300    async def invite_member_to_group(
2301        self,
2302        access_token: str,
2303        /,
2304        group_id: int,
2305        membership_id: int,
2306        membership_type: enums.MembershipType | int,
2307        *,
2308        message: str | None = None,
2309    ) -> typedefs.JSONObject:
2310        resp = await self._request(
2311            _POST,
2312            f"GroupV2/{group_id}/Members/IndividualInvite/{int(membership_type)}/{membership_id}/",
2313            auth=access_token,
2314            json={"message": str(message)},
2315        )
2316        assert isinstance(resp, dict)
2317        return resp
2318
2319    async def cancel_group_member_invite(
2320        self,
2321        access_token: str,
2322        /,
2323        group_id: int,
2324        membership_id: int,
2325        membership_type: enums.MembershipType | int,
2326    ) -> typedefs.JSONObject:
2327        resp = await self._request(
2328            _POST,
2329            f"GroupV2/{group_id}/Members/IndividualInviteCancel/{int(membership_type)}/{membership_id}/",
2330            auth=access_token,
2331        )
2332        assert isinstance(resp, dict)
2333        return resp
2334
2335    async def fetch_historical_definition(self) -> typedefs.JSONObject:
2336        resp = await self._request(_GET, "Destiny2/Stats/Definition/")
2337        assert isinstance(resp, dict)
2338        return resp
2339
2340    async def fetch_historical_stats(
2341        self,
2342        character_id: int,
2343        membership_id: int,
2344        membership_type: enums.MembershipType | int,
2345        day_start: datetime.datetime,
2346        day_end: datetime.datetime,
2347        groups: collections.Sequence[enums.StatsGroupType | int],
2348        modes: collections.Sequence[enums.GameMode | int],
2349        *,
2350        period_type: enums.PeriodType = enums.PeriodType.ALL_TIME,
2351    ) -> typedefs.JSONObject:
2352        end, start = time.parse_date_range(day_end, day_start)
2353        resp = await self._request(
2354            _GET,
2355            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/",
2356            json={
2357                "dayend": end,
2358                "daystart": start,
2359                "groups": [str(int(group)) for group in groups],
2360                "modes": [str(int(mode)) for mode in modes],
2361                "periodType": int(period_type),
2362            },
2363        )
2364        assert isinstance(resp, dict)
2365        return resp
2366
2367    async def fetch_historical_stats_for_account(
2368        self,
2369        membership_id: int,
2370        membership_type: enums.MembershipType | int,
2371        groups: collections.Sequence[enums.StatsGroupType | int],
2372    ) -> typedefs.JSONObject:
2373        resp = await self._request(
2374            _GET,
2375            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Stats/",
2376            json={"groups": [str(int(group)) for group in groups]},
2377        )
2378        assert isinstance(resp, dict)
2379        return resp
2380
2381    async def fetch_aggregated_activity_stats(
2382        self,
2383        character_id: int,
2384        membership_id: int,
2385        membership_type: enums.MembershipType | int,
2386        /,
2387    ) -> typedefs.JSONObject:
2388        resp = await self._request(
2389            _GET,
2390            f"Destiny2/{int(membership_type)}/Account/{membership_id}/"
2391            f"Character/{character_id}/Stats/AggregateActivityStats/",
2392        )
2393        assert isinstance(resp, dict)
2394        return resp
2395
2396    async def equip_loadout(
2397        self,
2398        access_token: str,
2399        /,
2400        loadout_index: int,
2401        character_id: int,
2402        membership_type: enums.MembershipType | int,
2403    ) -> None:
2404        response = await self._request(
2405            _POST,
2406            "Destiny2/Actions/Loadouts/EquipLoadout/",
2407            json={
2408                "loadoutIndex": loadout_index,
2409                "characterId": character_id,
2410                "membership_type": int(membership_type),
2411            },
2412            auth=access_token,
2413        )
2414        assert isinstance(response, int)
2415
2416    async def snapshot_loadout(
2417        self,
2418        access_token: str,
2419        /,
2420        loadout_index: int,
2421        character_id: int,
2422        membership_type: enums.MembershipType | int,
2423        *,
2424        color_hash: int | None = None,
2425        icon_hash: int | None = None,
2426        name_hash: int | None = None,
2427    ) -> None:
2428        response = await self._request(
2429            _POST,
2430            "Destiny2/Actions/Loadouts/SnapshotLoadout/",
2431            auth=access_token,
2432            json={
2433                "colorHash": color_hash,
2434                "iconHash": icon_hash,
2435                "nameHash": name_hash,
2436                "loadoutIndex": loadout_index,
2437                "characterId": character_id,
2438                "membershipType": int(membership_type),
2439            },
2440        )
2441        assert isinstance(response, int)
2442
2443    async def update_loadout(
2444        self,
2445        access_token: str,
2446        /,
2447        loadout_index: int,
2448        character_id: int,
2449        membership_type: enums.MembershipType | int,
2450        *,
2451        color_hash: int | None = None,
2452        icon_hash: int | None = None,
2453        name_hash: int | None = None,
2454    ) -> None:
2455        response = await self._request(
2456            _POST,
2457            "Destiny2/Actions/Loadouts/UpdateLoadoutIdentifiers/",
2458            auth=access_token,
2459            json={
2460                "colorHash": color_hash,
2461                "iconHash": icon_hash,
2462                "nameHash": name_hash,
2463                "loadoutIndex": loadout_index,
2464                "characterId": character_id,
2465                "membershipType": int(membership_type),
2466            },
2467        )
2468        assert isinstance(response, int)
2469
2470    async def clear_loadout(
2471        self,
2472        access_token: str,
2473        /,
2474        loadout_index: int,
2475        character_id: int,
2476        membership_type: enums.MembershipType | int,
2477    ) -> None:
2478        response = await self._request(
2479            _POST,
2480            "Destiny2/Actions/Loadouts/ClearLoadout/",
2481            json={
2482                "loadoutIndex": loadout_index,
2483                "characterId": character_id,
2484                "membership_type": int(membership_type),
2485            },
2486            auth=access_token,
2487        )
2488        assert isinstance(response, int)
2489
2490    async def force_drops_repair(self, access_token: str, /) -> bool:
2491        response = await self._request(
2492            _POST, "Tokens/Partner/ForceDropsRepair/", auth=access_token
2493        )
2494        assert isinstance(response, bool)
2495        return response
2496
2497    async def claim_partner_offer(
2498        self,
2499        access_token: str,
2500        /,
2501        *,
2502        offer_id: str,
2503        bungie_membership_id: int,
2504        transaction_id: str,
2505    ) -> bool:
2506        response = await self._request(
2507            _POST,
2508            "Tokens/Partner/ClaimOffer/",
2509            json={
2510                "PartnerOfferId": offer_id,
2511                "BungieNetMembershipId": bungie_membership_id,
2512                "TransactionId": transaction_id,
2513            },
2514            auth=access_token,
2515        )
2516        assert isinstance(response, bool)
2517        return response
2518
2519    async def fetch_bungie_rewards_for_user(
2520        self, access_token: str, /, membership_id: int
2521    ) -> typedefs.JSONObject:
2522        response = await self._request(
2523            _GET,
2524            f"Tokens/Rewards/GetRewardsForUser/{membership_id}/",
2525            auth=access_token,
2526        )
2527        assert isinstance(response, dict)
2528        return response
2529
2530    async def fetch_bungie_rewards_for_platform(
2531        self,
2532        access_token: str,
2533        /,
2534        membership_id: int,
2535        membership_type: enums.MembershipType | int,
2536    ) -> typedefs.JSONObject:
2537        response = await self._request(
2538            _GET,
2539            f"Tokens/Rewards/GetRewardsForPlatformUser/{membership_id}/{int(membership_type)}",
2540            auth=access_token,
2541        )
2542        assert isinstance(response, dict)
2543        return response
2544
2545    async def fetch_bungie_rewards(self) -> typedefs.JSONObject:
2546        response = await self._request(_GET, "Tokens/Rewards/BungieRewards/")
2547        assert isinstance(response, dict)
2548        return response
2549
2550    async def fetch_fireteam_listing(self, listing_id: int) -> typedefs.JSONObject:
2551        response = await self._request(_GET, f"FireteamFinder/Listing/{listing_id}/")
2552        assert isinstance(response, dict)
2553        return response

A single process REST client implementation.

This client is designed to only make HTTP requests and return raw JSON objects.

Example
import aiobungie

client = aiobungie.RESTClient("TOKEN")
async with client:
    response = await client.fetch_clan_members(4389205)
    for member in response['results']:
        print(member['destinyUserInfo'])
Parameters
  • token (str): A valid application token from Bungie's developer portal.
Other Parameters
  • client_secret (str | None): An optional application client secret, This is only needed if you're fetching OAuth2 tokens with this client.
  • client_id (int | None): An optional application client id, This is only needed if you're fetching OAuth2 tokens with this client.
  • settings (aiobungie.builders.Settings | None): The client settings to use, if None the default will be used.
  • owned_client (bool):
    • If set to True, this client will use the provided client_session parameter instead,
    • If set to True and client_session is None, ValueError will be raised.
    • If set to False, aiobungie will initialize a new client session for you.
  • client_session (aiohttp.ClientSession | None): If provided, this client session will be used to make all the HTTP requests. The owned_client must be set to True for this to work.
  • max_retries (int): The max retries number to retry if the request hit a 5xx status code.
  • debug (bool | str): Whether to enable logging responses or not.
Logging Levels
  • False: This will disable logging.
  • True: This will set the level to DEBUG and enable logging minimal information.
  • "TRACE" | aiobungie.TRACE: This will log the response headers along with the minimal information.
RESTClient( token: str, /, *, client_secret: str | None = None, client_id: int | None = None, settings: aiobungie.builders.Settings | None = None, owned_client: bool = True, client_session: aiohttp.client.ClientSession | None = None, dumps: Callable[[Mapping[str, typing.Any] | Sequence[Mapping[str, typing.Any]]], bytes] = <function dumps>, loads: Callable[[str | bytes], Sequence[Mapping[str, typing.Any]] | Mapping[str, typing.Any]] = <function loads>, max_retries: int = 4, debug: Union[Literal['TRACE'], bool, int] = False)
498    def __init__(
499        self,
500        token: str,
501        /,
502        *,
503        client_secret: str | None = None,
504        client_id: int | None = None,
505        settings: builders.Settings | None = None,
506        owned_client: bool = True,
507        client_session: aiohttp.ClientSession | None = None,
508        dumps: typedefs.Dumps = helpers.dumps,
509        loads: typedefs.Loads = helpers.loads,
510        max_retries: int = 4,
511        debug: typing.Literal["TRACE"] | bool | int = False,
512    ) -> None:
513        if owned_client is False and client_session is None:
514            raise ValueError(
515                "Expected an owned client session, but got `None`, Cannot have `owned_client` set to `False` and `client_session` to `None`"
516            )
517
518        self._settings = settings or builders.Settings()
519        self._session = client_session
520        self._owned_client = owned_client
521        self._lock: asyncio.Lock | None = None
522        self._client_secret = client_secret
523        self._client_id = client_id
524        self._token: str = token
525        self._max_retries = max_retries
526        self._dumps = dumps
527        self._loads = loads
528        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
529        self.with_debug(debug)
client_id: int | None
531    @property
532    def client_id(self) -> int | None:
533        return self._client_id

Return the client id of this REST client if provided, Otherwise None.

metadata: MutableMapping[typing.Any, typing.Any]
535    @property
536    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
537        return self._metadata

A mutable mapping storage for the user's needs.

This mapping is useful for storing any kind of data that the user may need to access later from a different process.

Example
import aiobungie

client = aiobungie.RESTClient(…)

async with client:
    # Fetch auth tokens and store them
    client.metadata["tokens"] = await client.fetch_access_token("code")

# Some other time.
async with client:
    # Retrieve the tokens
    tokens: aiobungie.OAuth2Response = client.metadata["tokens"]

    # Use them to fetch your user.
    user = await client.fetch_current_user_memberships(tokens.access_token)
is_alive: bool
539    @property
540    def is_alive(self) -> bool:
541        return self._session is not None

Returns True if the REST client is alive and False otherwise.

settings: aiobungie.builders.Settings
543    @property
544    def settings(self) -> builders.Settings:
545        return self._settings

Internal client settings used within the HTTP client session.

async def close(self) -> None:
547    async def close(self) -> None:
548        if self._session is None:
549            raise RuntimeError("REST client is not running.")
550
551        if self._owned_client:
552            await self._session.close()
553            self._session = None

Close this REST client session if it was acquired.

This method is automatically called when using async with contextmanager.

Raises
  • RuntimeError: If the client is already closed.
def open(self) -> None:
555    def open(self) -> None:
556        """Open a new client session. This is called internally with contextmanager usage."""
557        if self.is_alive and self._owned_client:
558            raise RuntimeError("Cannot open REST client when it's already open.")
559
560        if self._owned_client:
561            self._session = aiohttp.ClientSession(
562                connector=aiohttp.TCPConnector(
563                    use_dns_cache=self._settings.use_dns_cache,
564                    ttl_dns_cache=self._settings.ttl_dns_cache,
565                    ssl_context=self._settings.ssl_context,
566                    ssl=self._settings.ssl,
567                ),
568                connector_owner=True,
569                raise_for_status=False,
570                timeout=self._settings.http_timeout,
571                trust_env=self._settings.trust_env,
572                headers=self._settings.headers,
573            )

Open a new client session. This is called internally with contextmanager usage.

@typing.final
async def static_request( self, method: Literal['GET', 'DELETE', 'POST', 'PUT', 'PATCH'], path: str, *, auth: str | None = None, json: Mapping[str, typing.Any] | None = None, params: Mapping[str, typing.Any] | None = None) -> Mapping[str, typing.Any] | Sequence[Mapping[str, typing.Any]] | bytes | str | int | bool | None:
575    @typing.final
576    async def static_request(
577        self,
578        method: _HTTP_METHOD,
579        path: str,
580        *,
581        auth: str | None = None,
582        json: collections.Mapping[str, typing.Any] | None = None,
583        params: collections.Mapping[str, typing.Any] | None = None,
584    ) -> typedefs.JSONIsh:
585        return await self._request(method, path, auth=auth, json=json, params=params)

Perform an HTTP request given a valid Bungie endpoint.

This method allows you to freely perform HTTP requests to Bungie's API. It provides authentication support, JSON bodies, URL parameters and out of the box exception handling.

This method is useful for testing routes by yourself. or even calling routes that aiobungie doesn't support yet.

Parameters
  • method (str): The request method, This may be GET, POST, PUT, etc.
  • path (str): The Bungie endpoint or path. A path must look something like this Destiny2/3/Profile/46111239123/...
Other Parameters
  • auth (str | None): An optional bearer token for methods that requires OAuth2 Authorization header.
  • json (MutableMapping[str, typing.Any] | None): An optional JSON mapping to include in the request.
  • params (MutableMapping[str, typing.Any] | None): An optional URL query parameters mapping to include in the request.
Returns
  • aiobungie.typedefs.JSONIsh: The response payload.
@typing.final
def build_oauth2_url(self, client_id: int | None = None) -> aiobungie.builders.OAuthURL | None:
593    @typing.final
594    def build_oauth2_url(
595        self, client_id: int | None = None
596    ) -> builders.OAuthURL | None:
597        client_id = client_id or self._client_id
598        if client_id is None:
599            return None
600
601        return builders.OAuthURL(client_id=client_id)

Construct a new OAuthURL url object.

You can get the complete string representation of the url by calling .compile() on it.

Parameters
  • client_id (int | None): An optional client id to provide, If left None it will roll back to the id passed to the RESTClient, If both is None this method will return None.
Returns
async def fetch_oauth2_tokens(self, code: str, /) -> aiobungie.builders.OAuth2Response:
824    async def fetch_oauth2_tokens(self, code: str, /) -> builders.OAuth2Response:
825        data = {
826            "grant_type": "authorization_code",
827            "code": code,
828            "client_id": self._client_id,
829            "client_secret": self._client_secret,
830        }
831
832        response = await self._request(_POST, "", data=data, oauth2=True)
833        assert isinstance(response, dict)
834        return builders.OAuth2Response.build_response(response)

Makes a POST request and fetch the OAuth2 access_token and refresh token.

Parameters
  • code (str): The Authorization code received from the authorization endpoint found in the URL parameters.
Returns
Raises
async def refresh_access_token(self, refresh_token: str, /) -> aiobungie.builders.OAuth2Response:
836    async def refresh_access_token(
837        self, refresh_token: str, /
838    ) -> builders.OAuth2Response:
839        data = {
840            "grant_type": "refresh_token",
841            "refresh_token": refresh_token,
842            "client_id": self._client_id,
843            "client_secret": self._client_secret,
844        }
845
846        response = await self._request(_POST, "", data=data, oauth2=True)
847        assert isinstance(response, dict)
848        return builders.OAuth2Response.build_response(response)

Refresh OAuth2 access token given its refresh token.

Parameters
  • refresh_token (str): The refresh token.
Returns
async def fetch_bungie_user(self, id: int) -> Mapping[str, typing.Any]:
850    async def fetch_bungie_user(self, id: int) -> typedefs.JSONObject:
851        resp = await self._request(_GET, f"User/GetBungieNetUserById/{id}/")
852        assert isinstance(resp, dict)
853        return resp

Fetch a Bungie user by their id.

Parameters
  • id (int): The user id.
Returns
Raises
async def fetch_user_themes(self) -> Sequence[Mapping[str, typing.Any]]:
855    async def fetch_user_themes(self) -> typedefs.JSONArray:
856        resp = await self._request(_GET, "User/GetAvailableThemes/")
857        assert isinstance(resp, list)
858        return resp

Fetch all available user themes.

Returns
async def fetch_membership_from_id( self, id: int, type: aiobungie.MembershipType | int = <MembershipType.NONE: 0>, /) -> Mapping[str, typing.Any]:
860    async def fetch_membership_from_id(
861        self,
862        id: int,
863        type: enums.MembershipType | int = enums.MembershipType.NONE,
864        /,
865    ) -> typedefs.JSONObject:
866        resp = await self._request(_GET, f"User/GetMembershipsById/{id}/{int(type)}")
867        assert isinstance(resp, dict)
868        return resp

Fetch Bungie user's memberships from their id.

Parameters
Returns
Raises
async def fetch_membership( self, name: str, code: int, type: aiobungie.MembershipType | int = <MembershipType.ALL: -1>, /) -> Sequence[Mapping[str, typing.Any]]:
870    async def fetch_membership(
871        self,
872        name: str,
873        code: int,
874        type: enums.MembershipType | int = enums.MembershipType.ALL,
875        /,
876    ) -> typedefs.JSONArray:
877        resp = await self._request(
878            _POST,
879            f"Destiny2/SearchDestinyPlayerByBungieName/{int(type)}",
880            json={"displayName": name, "displayNameCode": code},
881        )
882        assert isinstance(resp, list)
883        return resp

Fetch a Destiny 2 Player.

Parameters
  • name (str): The unique Bungie player name.
  • code (int): The unique Bungie display name code.
  • type (aiobungie.aiobungie.MembershipType | int): The player's membership type, e,g. XBOX, STEAM, PSN
Returns
Raises
async def fetch_sanitized_membership(self, membership_id: int, /) -> Mapping[str, typing.Any]:
885    async def fetch_sanitized_membership(
886        self, membership_id: int, /
887    ) -> typedefs.JSONObject:
888        response = await self._request(
889            _GET, f"User/GetSanitizedPlatformDisplayNames/{membership_id}/"
890        )
891        assert isinstance(response, dict)
892        return response

Fetch a list of all display names linked to membership_id, Which is profanity filtered.

Parameters
  • membership_id (int): The membership ID to fetch
Returns
async def search_users(self, name: str, /) -> Mapping[str, typing.Any]:
894    async def search_users(self, name: str, /) -> typedefs.JSONObject:
895        resp = await self._request(
896            _POST,
897            "User/Search/GlobalName/0",
898            json={"displayNamePrefix": name},
899        )
900        assert isinstance(resp, dict)
901        return resp

Search for users by their global name and return all users who share this name.

Parameters
  • name (str): The user name.
Returns
Raises
async def fetch_clan_from_id( self, id: int, /, access_token: str | None = None) -> Mapping[str, typing.Any]:
903    async def fetch_clan_from_id(
904        self, id: int, /, access_token: str | None = None
905    ) -> typedefs.JSONObject:
906        resp = await self._request(_GET, f"GroupV2/{id}", auth=access_token)
907        assert isinstance(resp, dict)
908        return resp

Fetch a Bungie Clan by its id.

Parameters
  • id (int): The clan id.
Other Parameters
  • access_token (str | None): An optional access token to make the request with.

    If the token was bound to a member of the clan, This field aiobungie.crates.Clan.current_user_membership will be available and will return the membership of the user who made this request.

Returns
Raises
async def fetch_clan( self, name: str, /, access_token: str | None = None, *, type: aiobungie.GroupType | int = <GroupType.CLAN: 1>) -> Mapping[str, typing.Any]:
910    async def fetch_clan(
911        self,
912        name: str,
913        /,
914        access_token: str | None = None,
915        *,
916        type: enums.GroupType | int = enums.GroupType.CLAN,
917    ) -> typedefs.JSONObject:
918        resp = await self._request(
919            _GET, f"GroupV2/Name/{name}/{int(type)}", auth=access_token
920        )
921        assert isinstance(resp, dict)
922        return resp

Fetch a Clan by its name. This method will return the first clan found with given name name.

Parameters
  • name (str): The clan name.
Other Parameters
Returns
Raises
async def search_group( self, name: str, group_type: aiobungie.GroupType | int = <GroupType.CLAN: 1>, *, creation_date: aiobungie.GroupDate | int = 0, sort_by: int | None = None, group_member_count_filter: Optional[Literal[0, 1, 2, 3]] = None, locale_filter: str | None = None, tag_text: str | None = None, items_per_page: int | None = None, current_page: int | None = None, request_token: str | None = None) -> Mapping[str, typing.Any]:
924    async def search_group(
925        self,
926        name: str,
927        group_type: enums.GroupType | int = enums.GroupType.CLAN,
928        *,
929        creation_date: clans.GroupDate | int = 0,
930        sort_by: int | None = None,
931        group_member_count_filter: typing.Literal[0, 1, 2, 3] | None = None,
932        locale_filter: str | None = None,
933        tag_text: str | None = None,
934        items_per_page: int | None = None,
935        current_page: int | None = None,
936        request_token: str | None = None,
937    ) -> typedefs.JSONObject:
938        payload: collections.MutableMapping[str, typing.Any] = {"name": name}
939
940        # as the official documentation says, you're not allowed to use those fields
941        # on a clan search. it is safe to send the request with them being `null` but not filled with a value.
942        if (
943            group_type == enums.GroupType.CLAN
944            and group_member_count_filter is not None
945            and locale_filter
946            and tag_text
947        ):
948            raise ValueError(
949                "If you're searching for clans, (group_member_count_filter, locale_filter, tag_text) must be None."
950            )
951
952        payload["groupType"] = int(group_type)
953        payload["creationDate"] = int(creation_date)
954        payload["sortBy"] = sort_by
955        payload["groupMemberCount"] = group_member_count_filter
956        payload["locale"] = locale_filter
957        payload["tagText"] = tag_text
958        payload["itemsPerPage"] = items_per_page
959        payload["currentPage"] = current_page
960        payload["requestToken"] = request_token
961        payload["requestContinuationToken"] = request_token
962
963        resp = await self._request(_POST, "GroupV2/Search/", json=payload)
964        assert isinstance(resp, dict)
965        return resp

Search for groups.

If the group type is set to CLAN, then parameters group_member_count_filter, locale_filter and tag_text must be None, otherwise ValueError will be raised.

Parameters
  • name (str): The group name.
  • group_type (aiobungie.GroupType | int): The group type that's being searched for.
Other Parameters
  • creation_date (aiobungie.GroupDate | int): The creation date of the group. Defaults to 0 which is all time.
  • sort_by (int | None): ...
  • group_member_count_filter (int | None): ...
  • locale_filter (str | None): ...
  • tag_text (str | None): ...
  • items_per_page (int | None): ...
  • current_page (int | None): ...
  • request_token (str | None): ...
Returns
Raises
  • ValueError: If the group type is aiobungie.GroupType.CLAN and group_member_count_filter, locale_filter and tag_text are not None.
async def fetch_clan_admins(self, clan_id: int, /) -> Mapping[str, typing.Any]:
967    async def fetch_clan_admins(self, clan_id: int, /) -> typedefs.JSONObject:
968        resp = await self._request(_GET, f"GroupV2/{clan_id}/AdminsAndFounder/")
969        assert isinstance(resp, dict)
970        return resp

Fetch the admins and founder members of the clan.

Parameters
  • clan_id (int): The clan id.
Returns
Raises
async def fetch_clan_conversations(self, clan_id: int, /) -> Sequence[Mapping[str, typing.Any]]:
972    async def fetch_clan_conversations(self, clan_id: int, /) -> typedefs.JSONArray:
973        resp = await self._request(_GET, f"GroupV2/{clan_id}/OptionalConversations/")
974        assert isinstance(resp, list)
975        return resp

Fetch a clan's conversations.

Parameters
  • clan_id (int): The clan's id.
Returns
async def fetch_application(self, appid: int, /) -> Mapping[str, typing.Any]:
977    async def fetch_application(self, appid: int, /) -> typedefs.JSONObject:
978        resp = await self._request(_GET, f"App/Application/{appid}")
979        assert isinstance(resp, dict)
980        return resp

Fetch a Bungie Application.

Parameters
  • appid (int): The application id.
Returns
async def fetch_character( self, member_id: int, membership_type: aiobungie.MembershipType | int, character_id: int, components: Sequence[aiobungie.ComponentType], auth: str | None = None) -> Mapping[str, typing.Any]:
982    async def fetch_character(
983        self,
984        member_id: int,
985        membership_type: enums.MembershipType | int,
986        character_id: int,
987        components: collections.Sequence[enums.ComponentType],
988        auth: str | None = None,
989    ) -> typedefs.JSONObject:
990        collector = _collect_components(components)
991        response = await self._request(
992            _GET,
993            f"Destiny2/{int(membership_type)}/Profile/{member_id}/"
994            f"Character/{character_id}/?components={collector}",
995            auth=auth,
996        )
997        assert isinstance(response, dict)
998        return response

Fetch a Destiny 2 player's characters.

Parameters
Other Parameters
  • auth (str | None): A bearer access_token to make the request with. This is optional and limited to components that only requires an Authorization token.
Returns
Raises
async def fetch_activities( self, member_id: int, character_id: int, mode: aiobungie.GameMode | int, membership_type: aiobungie.MembershipType | int = <MembershipType.ALL: -1>, *, page: int = 0, limit: int = 1) -> Mapping[str, typing.Any]:
1000    async def fetch_activities(
1001        self,
1002        member_id: int,
1003        character_id: int,
1004        mode: enums.GameMode | int,
1005        membership_type: enums.MembershipType | int = enums.MembershipType.ALL,
1006        *,
1007        page: int = 0,
1008        limit: int = 1,
1009    ) -> typedefs.JSONObject:
1010        resp = await self._request(
1011            _GET,
1012            f"Destiny2/{int(membership_type)}/Account/"
1013            f"{member_id}/Character/{character_id}/Stats/Activities"
1014            f"/?mode={int(mode)}&count={limit}&page={page}",
1015        )
1016        assert isinstance(resp, dict)
1017        return resp

Fetch a Destiny 2 activity for the specified user id and character.

Parameters
  • member_id (int): The user id that starts with 4611.
  • character_id (int): The id of the character to retrieve.
  • mode (aiobungie.aiobungie.GameMode | int): This parameter filters the game mode, Nightfall, Strike, Iron Banner, etc.
Other Parameters
Returns
Raises
async def fetch_vendor_sales(self) -> Mapping[str, typing.Any]:
1019    async def fetch_vendor_sales(self) -> typedefs.JSONObject:
1020        resp = await self._request(
1021            _GET,
1022            f"Destiny2/Vendors/?components={int(enums.ComponentType.VENDOR_SALES)}",
1023        )
1024        assert isinstance(resp, dict)
1025        return resp
async def fetch_profile( self, membership_id: int, type: aiobungie.MembershipType | int, components: Sequence[aiobungie.ComponentType], auth: str | None = None) -> Mapping[str, typing.Any]:
1027    async def fetch_profile(
1028        self,
1029        membership_id: int,
1030        type: enums.MembershipType | int,
1031        components: collections.Sequence[enums.ComponentType],
1032        auth: str | None = None,
1033    ) -> typedefs.JSONObject:
1034        collector = _collect_components(components)
1035        response = await self._request(
1036            _GET,
1037            f"Destiny2/{int(type)}/Profile/{membership_id}/?components={collector}",
1038            auth=auth,
1039        )
1040        assert isinstance(response, dict)
1041        return response

Fetch a bungie profile.

Parameters
Other Parameters
  • auth (str | None): A bearer access_token to make the request with. This is optional and limited to components that only requires an Authorization token.
Returns
Raises
async def fetch_entity(self, type: str, hash: int) -> Mapping[str, typing.Any]:
1043    async def fetch_entity(self, type: str, hash: int) -> typedefs.JSONObject:
1044        response = await self._request(_GET, route=f"Destiny2/Manifest/{type}/{hash}")
1045        assert isinstance(response, dict)
1046        return response

Fetch a Destiny definition item given its type and hash.

Parameters
  • type (str): Entity's type definition.
  • hash (int): Entity's hash.
Returns
async def fetch_inventory_item(self, hash: int, /) -> Mapping[str, typing.Any]:
1048    async def fetch_inventory_item(self, hash: int, /) -> typedefs.JSONObject:
1049        resp = await self.fetch_entity("DestinyInventoryItemDefinition", hash)
1050        assert isinstance(resp, dict)
1051        return resp

Fetch a Destiny inventory item entity given a its hash.

Parameters
  • hash (int): Entity's hash.
Returns
async def fetch_objective_entity(self, hash: int, /) -> Mapping[str, typing.Any]:
1053    async def fetch_objective_entity(self, hash: int, /) -> typedefs.JSONObject:
1054        resp = await self.fetch_entity("DestinyObjectiveDefinition", hash)
1055        assert isinstance(resp, dict)
1056        return resp

Fetch a Destiny objective entity given a its hash.

Parameters
  • hash (int): objective's hash.
Returns
async def fetch_groups_for_member( self, member_id: int, member_type: aiobungie.MembershipType | int, /, *, filter: int = 0, group_type: aiobungie.GroupType | int = <GroupType.CLAN: 1>) -> Mapping[str, typing.Any]:
1058    async def fetch_groups_for_member(
1059        self,
1060        member_id: int,
1061        member_type: enums.MembershipType | int,
1062        /,
1063        *,
1064        filter: int = 0,
1065        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1066    ) -> typedefs.JSONObject:
1067        resp = await self._request(
1068            _GET,
1069            f"GroupV2/User/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1070        )
1071        assert isinstance(resp, dict)
1072        return resp

Fetch the information about the groups for a member.

Parameters
Other Parameters
Returns
async def fetch_potential_groups_for_member( self, member_id: int, member_type: aiobungie.MembershipType | int, /, *, filter: int = 0, group_type: aiobungie.GroupType | int = <GroupType.CLAN: 1>) -> Mapping[str, typing.Any]:
1074    async def fetch_potential_groups_for_member(
1075        self,
1076        member_id: int,
1077        member_type: enums.MembershipType | int,
1078        /,
1079        *,
1080        filter: int = 0,
1081        group_type: enums.GroupType | int = enums.GroupType.CLAN,
1082    ) -> typedefs.JSONObject:
1083        resp = await self._request(
1084            _GET,
1085            f"GroupV2/User/Potential/{int(member_type)}/{member_id}/{filter}/{int(group_type)}/",
1086        )
1087        assert isinstance(resp, dict)
1088        return resp

Get information about the groups that a given member has applied to or been invited to.

Parameters
Other Parameters
Returns
async def fetch_clan_members( self, clan_id: int, /, *, name: str | None = None, type: aiobungie.MembershipType | int = <MembershipType.NONE: 0>) -> Mapping[str, typing.Any]:
1090    async def fetch_clan_members(
1091        self,
1092        clan_id: int,
1093        /,
1094        *,
1095        name: str | None = None,
1096        type: enums.MembershipType | int = enums.MembershipType.NONE,
1097    ) -> typedefs.JSONObject:
1098        resp = await self._request(
1099            _GET,
1100            f"/GroupV2/{clan_id}/Members/?memberType={int(type)}&nameSearch={name if name else ''}&currentpage=1",
1101        )
1102        assert isinstance(resp, dict)
1103        return resp

Fetch all Bungie Clan members.

Parameters
  • clan_id (int): The clans id
Other Parameters
Returns
Raises
async def fetch_hardlinked_credentials( self, credential: int, type: aiobungie.CredentialType | int = <CredentialType.STEAMID: 12>, /) -> Mapping[str, typing.Any]:
1105    async def fetch_hardlinked_credentials(
1106        self,
1107        credential: int,
1108        type: enums.CredentialType | int = enums.CredentialType.STEAMID,
1109        /,
1110    ) -> typedefs.JSONObject:
1111        resp = await self._request(
1112            _GET,
1113            f"User/GetMembershipFromHardLinkedCredential/{int(type)}/{credential}/",
1114        )
1115        assert isinstance(resp, dict)
1116        return resp

Gets any hard linked membership given a credential.

Only works for credentials that are public just aiobungie.CredentialType.STEAMID right now. Cross Save aware.

Parameters
  • credential (int): A valid SteamID64
  • type (aiobungie.aiobungie.CredentialType | int): The credential type. This must not be changed Since its only credential that works "currently"
Returns
async def fetch_user_credentials( self, access_token: str, membership_id: int, /) -> Sequence[Mapping[str, typing.Any]]:
1118    async def fetch_user_credentials(
1119        self, access_token: str, membership_id: int, /
1120    ) -> typedefs.JSONArray:
1121        resp = await self._request(
1122            _GET,
1123            f"User/GetCredentialTypesForTargetAccount/{membership_id}",
1124            auth=access_token,
1125        )
1126        assert isinstance(resp, list)
1127        return resp

Fetch an array of credential types attached to the requested account.

This method require OAuth2 Bearer access token.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • membership_id (int): The id of the membership to return.
Returns
Raises
async def insert_socket_plug( self, action_token: str, /, instance_id: int, plug: aiobungie.builders.PlugSocketBuilder | Mapping[str, int], character_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
1129    async def insert_socket_plug(
1130        self,
1131        action_token: str,
1132        /,
1133        instance_id: int,
1134        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1135        character_id: int,
1136        membership_type: enums.MembershipType | int,
1137    ) -> typedefs.JSONObject:
1138        if isinstance(plug, builders.PlugSocketBuilder):
1139            plug = plug.collect()
1140
1141        body = {
1142            "actionToken": action_token,
1143            "itemInstanceId": instance_id,
1144            "plug": plug,
1145            "characterId": character_id,
1146            "membershipType": int(membership_type),
1147        }
1148        resp = await self._request(
1149            _POST, "Destiny2/Actions/Items/InsertSocketPlug", json=body
1150        )
1151        assert isinstance(resp, dict)
1152        return resp

Insert a plug into a socketed item.

OAuth2: AdvancedWriteActions scope is required

Parameters
  • action_token (str): Action token provided by the AwaGetActionToken API call.
  • instance_id (int): The item instance id that's plug inserted.
  • plug (aiobungie.builders.PlugSocketBuilder | Mapping[str, int]): Either a PlugSocketBuilder object or a raw dict contains key, value for the plug entries.
Example
plug = (
    aiobungie.PlugSocketBuilder()
    .set_socket_array(0)
    .set_socket_index(0)
    .set_plug_item(3023847)
    .collect()
)
await insert_socket_plug_free(..., plug=plug)

character_id : int The character's id. membership_type : aiobungie.aiobungie.MembershipType | int The membership type.

Returns
Raises
async def insert_socket_plug_free( self, access_token: str, /, instance_id: int, plug: aiobungie.builders.PlugSocketBuilder | Mapping[str, int], character_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
1154    async def insert_socket_plug_free(
1155        self,
1156        access_token: str,
1157        /,
1158        instance_id: int,
1159        plug: builders.PlugSocketBuilder | collections.Mapping[str, int],
1160        character_id: int,
1161        membership_type: enums.MembershipType | int,
1162    ) -> typedefs.JSONObject:
1163        if isinstance(plug, builders.PlugSocketBuilder):
1164            plug = plug.collect()
1165
1166        body = {
1167            "itemInstanceId": instance_id,
1168            "plug": plug,
1169            "characterId": character_id,
1170            "membershipType": int(membership_type),
1171        }
1172        resp = await self._request(
1173            _POST,
1174            "Destiny2/Actions/Items/InsertSocketPlugFree",
1175            json=body,
1176            auth=access_token,
1177        )
1178        assert isinstance(resp, dict)
1179        return resp

Insert a plug into a socketed item. This doesn't require an Action token.

OAuth2: MoveEquipDestinyItems scope is required

Parameters
  • instance_id (int): The item instance id that's plug inserted.
  • plug (aiobungie.builders.PlugSocketBuilder | Mapping[str, int]): Either a PlugSocketBuilder object or a raw dict contains key, value for the plug entries.
Example
plug = (
    aiobungie.PlugSocketBuilder()
    .set_socket_array(0)
    .set_socket_index(0)
    .set_plug_item(3023847)
    .collect()
)
await insert_socket_plug_free(..., plug=plug)

character_id : int The character's id. membership_type : aiobungie.aiobungie.MembershipType | int The membership type.

Returns
Raises
@helpers.unstable
async def set_item_lock_state( self, access_token: str, state: bool, /, item_id: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> int:
1181    @helpers.unstable
1182    async def set_item_lock_state(
1183        self,
1184        access_token: str,
1185        state: bool,
1186        /,
1187        item_id: int,
1188        character_id: int,
1189        membership_type: enums.MembershipType | int,
1190    ) -> int:
1191        body = {
1192            "state": state,
1193            "itemId": item_id,
1194            "characterId": character_id,
1195            "membershipType": int(membership_type),
1196        }
1197        response = await self._request(
1198            _POST,
1199            "Destiny2/Actions/Items/SetLockState",
1200            json=body,
1201            auth=access_token,
1202        )
1203        assert isinstance(response, int)
1204        return response

Set the Lock State for an instanced item.

OAuth2: MoveEquipDestinyItems scope is required

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • state (bool): If True, The item will be locked, If False, The item will be unlocked.
  • item_id (int): The item id.
  • character_id (int): The character id.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The membership type for the associated account.
Returns
  • int: An integer represents whether the request was successful or failed.
Raises
async def set_quest_track_state( self, access_token: str, state: bool, /, item_id: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> int:
1206    async def set_quest_track_state(
1207        self,
1208        access_token: str,
1209        state: bool,
1210        /,
1211        item_id: int,
1212        character_id: int,
1213        membership_type: enums.MembershipType | int,
1214    ) -> int:
1215        body = {
1216            "state": state,
1217            "itemId": item_id,
1218            "characterId": character_id,
1219            "membership_type": int(membership_type),
1220        }
1221        response = await self._request(
1222            _POST,
1223            "Destiny2/Actions/Items/SetTrackedState",
1224            json=body,
1225            auth=access_token,
1226        )
1227        assert isinstance(response, int)
1228        return response

Set the Tracking State for an instanced Quest or Bounty.

OAuth2: MoveEquipDestinyItems scope is required

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • state (bool): If True, The item will be locked, If False, The item will be unlocked.
  • item_id (int): The item id.
  • character_id (int): The character id.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The membership type for the associated account.
Returns
  • int: An integer represents whether the request was successful or failed.
Raises
async def fetch_manifest_path(self) -> Mapping[str, typing.Any]:
1230    async def fetch_manifest_path(self) -> typedefs.JSONObject:
1231        path = await self._request(_GET, "Destiny2/Manifest")
1232        assert isinstance(path, dict)
1233        return path

Fetch the manifest JSON paths.

Returns
  • typedefs.JSONObject: The manifest JSON paths.
async def read_manifest_bytes( self, language: Literal['en', 'fr', 'es', 'es-mx', 'de', 'it', 'ja', 'pt-br', 'ru', 'pl', 'ko', 'zh-cht', 'zh-chs'] = 'en', /) -> bytes:
1235    async def read_manifest_bytes(self, language: _ALLOWED_LANGS = "en", /) -> bytes:
1236        _ensure_manifest_language(language)
1237
1238        content = await self.fetch_manifest_path()
1239        resp = await self._request(
1240            _GET,
1241            content["mobileWorldContentPaths"][language],
1242            unwrap_bytes=True,
1243            base=True,
1244        )
1245        assert isinstance(resp, bytes)
1246        return resp

Read raw manifest SQLite database bytes response.

This method can be used to write the bytes to zipped file and then extract it to get the manifest content.

Parameters
  • language (str): The manifest database language bytes to get.
Returns
  • bytes: The bytes to read and write the manifest database.
async def download_sqlite_manifest( self, language: Literal['en', 'fr', 'es', 'es-mx', 'de', 'it', 'ja', 'pt-br', 'ru', 'pl', 'ko', 'zh-cht', 'zh-chs'] = 'en', name: str = 'manifest', path: pathlib.Path | str = '.', *, force: bool = False, executor: concurrent.futures._base.Executor | None = None) -> pathlib.Path:
1248    async def download_sqlite_manifest(
1249        self,
1250        language: _ALLOWED_LANGS = "en",
1251        name: str = "manifest",
1252        path: pathlib.Path | str = ".",
1253        *,
1254        force: bool = False,
1255        executor: concurrent.futures.Executor | None = None,
1256    ) -> pathlib.Path:
1257        complete_path = _get_path(name, path, sql=True)
1258
1259        if complete_path.exists():
1260            if force:
1261                _LOGGER.info(
1262                    f"Found manifest in {complete_path!s}. Forcing to Re-Download."
1263                )
1264                complete_path.unlink(missing_ok=True)
1265
1266                return await self.download_sqlite_manifest(
1267                    language, name, path, force=force
1268                )
1269
1270            else:
1271                raise FileExistsError(
1272                    "Manifest file already exists, "
1273                    "To force download, set the `force` parameter to `True`."
1274                )
1275
1276        _LOGGER.info(f"Downloading manifest. Location: {complete_path!s}")
1277        data_bytes = await self.read_manifest_bytes(language)
1278        await asyncio.get_running_loop().run_in_executor(
1279            executor, _write_sqlite_bytes, data_bytes, path, name
1280        )
1281        _LOGGER.info("Finished downloading manifest.")
1282        return _get_path(name, path, sql=True)

Downloads the SQLite version of Destiny2's Manifest.

Example
manifest = await rest.download_sqlite_manifest()
with sqlite3.connect(manifest) as conn:
    ...
Parameters
  • language (str): The manifest language to download, Default is English.
  • path (str | pathlib.Path): The path to download this manifest. Example "/tmp/databases/", Default is the current directory.
  • name (str): The manifest database file name. Default is manifest
  • force (bool): Whether to force the download. Default is False. However if set to true the old file will get unlinked and a new one will begin to download.
  • executor (concurrent.futures.Executor | None): An optional executor which will be used to write the bytes of the manifest.
Returns
  • pathlib.Path: A pathlib object of the .sqlite file.
Raises
  • FileExistsError: If the manifest file exists and force is False.
  • ValueError: If the provided language was not recognized.
async def download_json_manifest( self, file_name: str = 'manifest', path: str | pathlib.Path = '.', *, language: Literal['en', 'fr', 'es', 'es-mx', 'de', 'it', 'ja', 'pt-br', 'ru', 'pl', 'ko', 'zh-cht', 'zh-chs'] = 'en', executor: concurrent.futures._base.Executor | None = None) -> pathlib.Path:
1284    async def download_json_manifest(
1285        self,
1286        file_name: str = "manifest",
1287        path: str | pathlib.Path = ".",
1288        *,
1289        language: _ALLOWED_LANGS = "en",
1290        executor: concurrent.futures.Executor | None = None,
1291    ) -> pathlib.Path:
1292        _ensure_manifest_language(language)
1293        full_path = _get_path(file_name, path)
1294        _LOGGER.info(f"Downloading manifest JSON to {full_path!r}...")
1295
1296        content = await self.fetch_manifest_path()
1297        json_bytes = await self._request(
1298            _GET,
1299            content["jsonWorldContentPaths"][language],
1300            unwrap_bytes=True,
1301            base=True,
1302        )
1303
1304        assert isinstance(json_bytes, bytes)
1305        await asyncio.get_running_loop().run_in_executor(
1306            executor, _write_json_bytes, json_bytes, file_name, path
1307        )
1308        _LOGGER.info("Finished downloading manifest JSON.")
1309        return full_path

Download the Bungie manifest json file.

Example
manifest = await rest.download_json_manifest()
with open(manifest, "r") as f:
    to_dict = json.loads(f.read())
    item_definitions = to_dict['DestinyInventoryItemDefinition']
Parameters
  • file_name (str): The file name to save the manifest json file. Default is manifest.
  • path (str | pathlib.Path): The path to save the manifest json file. Default is the current directory. Example "D:/"
  • language (str): The manifest database language bytes to get. Default is English.
  • executor (concurrent.futures.Executor | None): An optional executor which will be used to write the bytes of the manifest.
Returns
  • pathlib.Path: The path of this JSON manifest.
async def fetch_manifest_version(self) -> str:
1311    async def fetch_manifest_version(self) -> str:
1312        # This is guaranteed str.
1313        return (await self.fetch_manifest_path())["version"]

Fetch the manifest version.

Returns
  • str: The manifest version.
async def fetch_linked_profiles( self, member_id: int, member_type: aiobungie.MembershipType | int, /, *, all: bool = False) -> Mapping[str, typing.Any]:
1315    async def fetch_linked_profiles(
1316        self,
1317        member_id: int,
1318        member_type: enums.MembershipType | int,
1319        /,
1320        *,
1321        all: bool = False,
1322    ) -> typedefs.JSONObject:
1323        resp = await self._request(
1324            _GET,
1325            f"Destiny2/{int(member_type)}/Profile/{member_id}/LinkedProfiles/?getAllMemberships={all}",
1326        )
1327        assert isinstance(resp, dict)
1328        return resp

Returns a summary information about all profiles linked to the requested member.

The passed membership id/type maybe a Bungie.Net membership or a Destiny memberships.

It will only return linked accounts whose linkages you are allowed to view.

Parameters
  • member_id (int): The ID of the membership. This must be a valid Bungie.Net or PSN or Xbox ID.
  • member_type (aiobungie.aiobungie.MembershipType | int): The type for the membership whose linked Destiny account you want to return.
Other Parameters
  • all (bool): If provided and set to True, All memberships regardless of whether they're obscured by overrides will be returned,

    If provided and set to False, Only available memberships will be returned. The default for this is False.

Returns
async def fetch_clan_banners(self) -> Mapping[str, typing.Any]:
1330    async def fetch_clan_banners(self) -> typedefs.JSONObject:
1331        resp = await self._request(_GET, "Destiny2/Clan/ClanBannerDictionary/")
1332        assert isinstance(resp, dict)
1333        return resp

Fetch the values of the clan banners.

Returns
async def fetch_public_milestones(self) -> Mapping[str, typing.Any]:
1335    async def fetch_public_milestones(self) -> typedefs.JSONObject:
1336        resp = await self._request(_GET, "Destiny2/Milestones/")
1337        assert isinstance(resp, dict)
1338        return resp

Fetch the available milestones.

Returns
async def fetch_public_milestone_content(self, milestone_hash: int, /) -> Mapping[str, typing.Any]:
1340    async def fetch_public_milestone_content(
1341        self, milestone_hash: int, /
1342    ) -> typedefs.JSONObject:
1343        resp = await self._request(
1344            _GET, f"Destiny2/Milestones/{milestone_hash}/Content/"
1345        )
1346        assert isinstance(resp, dict)
1347        return resp

Fetch the milestone content given its hash.

Parameters
  • milestone_hash (int): The milestone hash.
Returns
async def fetch_current_user_memberships(self, access_token: str, /) -> Mapping[str, typing.Any]:
1349    async def fetch_current_user_memberships(
1350        self, access_token: str, /
1351    ) -> typedefs.JSONObject:
1352        resp = await self._request(
1353            _GET,
1354            "User/GetMembershipsForCurrentUser/",
1355            auth=access_token,
1356        )
1357        assert isinstance(resp, dict)
1358        return resp

Fetch a bungie user's accounts with the signed in user. This GET method requires a Bearer access token for the authorization.

This requires OAuth2 scope enabled and the valid Bearer access_token.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
Returns
async def equip_item( self, access_token: str, /, item_id: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> None:
1360    async def equip_item(
1361        self,
1362        access_token: str,
1363        /,
1364        item_id: int,
1365        character_id: int,
1366        membership_type: enums.MembershipType | int,
1367    ) -> None:
1368        payload = {
1369            "itemId": item_id,
1370            "characterId": character_id,
1371            "membershipType": int(membership_type),
1372        }
1373
1374        await self._request(
1375            _POST,
1376            "Destiny2/Actions/Items/EquipItem/",
1377            json=payload,
1378            auth=access_token,
1379        )

Equip an item to a character.

This requires the OAuth2: MoveEquipDestinyItems scope. Also You must have a valid Destiny account, and either be in a social space, in orbit or offline.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • item_id (int): The item id.
  • character_id (int): The character's id to equip the item to.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The membership type associated with this player.
async def equip_items( self, access_token: str, /, item_ids: Sequence[int], character_id: int, membership_type: aiobungie.MembershipType | int) -> None:
1381    async def equip_items(
1382        self,
1383        access_token: str,
1384        /,
1385        item_ids: collections.Sequence[int],
1386        character_id: int,
1387        membership_type: enums.MembershipType | int,
1388    ) -> None:
1389        payload = {
1390            "itemIds": item_ids,
1391            "characterId": character_id,
1392            "membershipType": int(membership_type),
1393        }
1394        await self._request(
1395            _POST,
1396            "Destiny2/Actions/Items/EquipItems/",
1397            json=payload,
1398            auth=access_token,
1399        )

Equip multiple items to a character.

This requires the OAuth2: MoveEquipDestinyItems scope. Also You must have a valid Destiny account, and either be in a social space, in orbit or offline.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • item_ids (Sequence[int]): A sequence of item ids.
  • character_id (int): The character's id to equip the item to.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The membership type associated with this player.
async def ban_clan_member( self, access_token: str, /, group_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, *, length: int = 0, comment: str | None = None) -> None:
1401    async def ban_clan_member(
1402        self,
1403        access_token: str,
1404        /,
1405        group_id: int,
1406        membership_id: int,
1407        membership_type: enums.MembershipType | int,
1408        *,
1409        length: int = 0,
1410        comment: str | None = None,
1411    ) -> None:
1412        payload = {"comment": str(comment), "length": length}
1413        await self._request(
1414            _POST,
1415            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Ban/",
1416            json=payload,
1417            auth=access_token,
1418        )

Bans a member from the clan.

This request requires OAuth2: oauth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group id.
  • membership_id (int): The member id to ban.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The member's membership type.
Other Parameters
  • length (int): An optional ban length.
  • comment (aiobungie.UndefinedOr[str]): An optional comment to this ban. Default is UNDEFINED
async def unban_clan_member( self, access_token: str, /, group_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int) -> None:
1420    async def unban_clan_member(
1421        self,
1422        access_token: str,
1423        /,
1424        group_id: int,
1425        membership_id: int,
1426        membership_type: enums.MembershipType | int,
1427    ) -> None:
1428        await self._request(
1429            _POST,
1430            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Unban/",
1431            auth=access_token,
1432        )

Unban a member from the clan.

This request requires OAuth2: oauth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group id.
  • membership_id (int): The member id to unban.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The member's membership type.
async def kick_clan_member( self, access_token: str, /, group_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
1434    async def kick_clan_member(
1435        self,
1436        access_token: str,
1437        /,
1438        group_id: int,
1439        membership_id: int,
1440        membership_type: enums.MembershipType | int,
1441    ) -> typedefs.JSONObject:
1442        resp = await self._request(
1443            _POST,
1444            f"GroupV2/{group_id}/Members/{int(membership_type)}/{membership_id}/Kick/",
1445            auth=access_token,
1446        )
1447        assert isinstance(resp, dict)
1448        return resp

Kick a member from the clan.

This request requires OAuth2: oauth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group id.
  • membership_id (int): The member id to kick.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The member's membership type.
Returns
async def edit_clan( self, access_token: str, /, group_id: int, *, name: str | None = None, about: str | None = None, motto: str | None = None, theme: str | None = None, tags: Sequence[str] | None = None, is_public: bool | None = None, locale: str | None = None, avatar_image_index: int | None = None, membership_option: aiobungie.MembershipOption | int | None = None, allow_chat: bool | None = None, chat_security: Optional[Literal[0, 1]] = None, call_sign: str | None = None, homepage: Optional[Literal[0, 1, 2]] = None, enable_invite_messaging_for_admins: bool | None = None, default_publicity: Optional[Literal[0, 1, 2]] = None, is_public_topic_admin: bool | None = None) -> None:
1450    async def edit_clan(
1451        self,
1452        access_token: str,
1453        /,
1454        group_id: int,
1455        *,
1456        name: str | None = None,
1457        about: str | None = None,
1458        motto: str | None = None,
1459        theme: str | None = None,
1460        tags: collections.Sequence[str] | None = None,
1461        is_public: bool | None = None,
1462        locale: str | None = None,
1463        avatar_image_index: int | None = None,
1464        membership_option: enums.MembershipOption | int | None = None,
1465        allow_chat: bool | None = None,
1466        chat_security: typing.Literal[0, 1] | None = None,
1467        call_sign: str | None = None,
1468        homepage: typing.Literal[0, 1, 2] | None = None,
1469        enable_invite_messaging_for_admins: bool | None = None,
1470        default_publicity: typing.Literal[0, 1, 2] | None = None,
1471        is_public_topic_admin: bool | None = None,
1472    ) -> None:
1473        payload = {
1474            "name": name,
1475            "about": about,
1476            "motto": motto,
1477            "theme": theme,
1478            "tags": tags,
1479            "isPublic": is_public,
1480            "avatarImageIndex": avatar_image_index,
1481            "isPublicTopicAdminOnly": is_public_topic_admin,
1482            "allowChat": allow_chat,
1483            "chatSecurity": chat_security,
1484            "callsign": call_sign,
1485            "homepage": homepage,
1486            "enableInvitationMessagingForAdmins": enable_invite_messaging_for_admins,
1487            "defaultPublicity": default_publicity,
1488            "locale": locale,
1489        }
1490        if membership_option is not None:
1491            payload["membershipOption"] = int(membership_option)
1492
1493        await self._request(
1494            _POST,
1495            f"GroupV2/{group_id}/Edit",
1496            json=payload,
1497            auth=access_token,
1498        )

Edit a clan.

Notes
  • This request requires OAuth2: oauth2: AdminGroups scope.
  • All arguments will default to None if not provided. This does not include access_token and group_id
Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group id to edit.
Other Parameters
  • name (str | None): The name to edit the clan with.
  • about (str | None): The about section to edit the clan with.
  • motto (str | None): The motto section to edit the clan with.
  • theme (str | None): The theme name to edit the clan with.
  • tags (collections.Sequence[str] | None): A sequence of strings to replace the clan tags with.
  • is_public (bool | None): If provided and set to True, The clan will set to private. If provided and set to False, The clan will set to public whether it was or not.
  • locale (str | None): The locale section to edit the clan with.
  • avatar_image_index (int | None): The clan avatar image index to edit the clan with.
  • membership_option : aiobungie.typedefs.NoneOr[aiobungie.aiobungie.MembershipOption | int] # noqa (E501 # Line too long): The clan membership option to edit it with.
  • allow_chat (bool | None): If provided and set to True, The clan members will be allowed to chat. If provided and set to False, The clan members will not be allowed to chat.
  • chat_security (aiobungie.typedefs.NoneOr[typing.Literal[0, 1]]): If provided and set to 0, The clan chat security will be edited to Group only. If provided and set to 1, The clan chat security will be edited to Admin only.
  • call_sign (str | None): The clan call sign to edit it with.
  • homepage (aiobungie.typedefs.NoneOr[typing.Literal[0, 1, 2]]): If provided and set to 0, The clan chat homepage will be edited to Wall. If provided and set to 1, The clan chat homepage will be edited to Forum. If provided and set to 0, The clan chat homepage will be edited to AllianceForum.
  • enable_invite_messaging_for_admins (bool | None): ???
  • default_publicity (aiobungie.typedefs.NoneOr[typing.Literal[0, 1, 2]]): If provided and set to 0, The clan chat publicity will be edited to Public. If provided and set to 1, The clan chat publicity will be edited to Alliance. If provided and set to 2, The clan chat publicity will be edited to Private.
  • is_public_topic_admin (bool | None): ???
async def edit_clan_options( self, access_token: str, /, group_id: int, *, invite_permissions_override: bool | None = None, update_culture_permissionOverride: bool | None = None, host_guided_game_permission_override: Optional[Literal[0, 1, 2]] = None, update_banner_permission_override: bool | None = None, join_level: aiobungie.ClanMemberType | int | None = None) -> None:
1500    async def edit_clan_options(
1501        self,
1502        access_token: str,
1503        /,
1504        group_id: int,
1505        *,
1506        invite_permissions_override: bool | None = None,
1507        update_culture_permissionOverride: bool | None = None,
1508        host_guided_game_permission_override: typing.Literal[0, 1, 2] | None = None,
1509        update_banner_permission_override: bool | None = None,
1510        join_level: enums.ClanMemberType | int | None = None,
1511    ) -> None:
1512        payload = {
1513            "InvitePermissionOverride": invite_permissions_override,
1514            "UpdateCulturePermissionOverride": update_culture_permissionOverride,
1515            "HostGuidedGamePermissionOverride": host_guided_game_permission_override,
1516            "UpdateBannerPermissionOverride": update_banner_permission_override,
1517            "JoinLevel": int(join_level) if join_level else None,
1518        }
1519
1520        await self._request(
1521            _POST,
1522            f"GroupV2/{group_id}/EditFounderOptions",
1523            json=payload,
1524            auth=access_token,
1525        )

Edit the clan options.

Notes
  • This request requires OAuth2: oauth2: AdminGroups scope.
  • All arguments will default to None if not provided. This does not include access_token and group_id
Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group id.
Other Parameters
  • invite_permissions_override (bool | None): Minimum Member Level allowed to invite new members to group Always Allowed: Founder, Acting Founder True means admins have this power, false means they don't Default is False for clans, True for groups.
  • update_culture_permissionOverride (bool | None): Minimum Member Level allowed to update group culture Always Allowed: Founder, Acting Founder True means admins have this power, false means they don't Default is False for clans, True for groups.
  • host_guided_game_permission_override (aiobungie.typedefs.NoneOr[typing.Literal[0, 1, 2]]): Minimum Member Level allowed to host guided games Always Allowed: Founder, Acting Founder, Admin Allowed Overrides: 0 -> None, 1 -> Beginner 2 -> Member. Default is Member for clans, None for groups, although this means nothing for groups.
  • update_banner_permission_override (bool | None): Minimum Member Level allowed to update banner Always Allowed: Founder, Acting Founder True means admins have this power, false means they don't Default is False for clans, True for groups.
  • join_level (aiobungie.ClanMemberType): Level to join a member at when accepting an invite, application, or joining an open clan. Default is aiobungie.ClanMemberType.BEGINNER
async def report_player( self, access_token: str, /, activity_id: int, character_id: int, reason_hashes: Sequence[int], reason_category_hashes: Sequence[int]) -> None:
1527    async def report_player(
1528        self,
1529        access_token: str,
1530        /,
1531        activity_id: int,
1532        character_id: int,
1533        reason_hashes: collections.Sequence[int],
1534        reason_category_hashes: collections.Sequence[int],
1535    ) -> None:
1536        await self._request(
1537            _POST,
1538            f"Destiny2/Stats/PostGameCarnageReport/{activity_id}/Report/",
1539            json={
1540                "reasonCategoryHashes": reason_category_hashes,
1541                "reasonHashes": reason_hashes,
1542                "offendingCharacterId": character_id,
1543            },
1544            auth=access_token,
1545        )

Report a player that you met in an activity that was engaging in ToS-violating activities.

This method requires BnetWrite OAuth2 scope.

Both you and the offending player must have played in the activity_id passed in. Please use this judiciously and only when you have strong suspicions of violation, pretty please.

Parameters
  • access_token (str): The bearer access token associated with the bungie account that will be used to make the request with.
  • activity_id (int): The activity where you ran into the person you're reporting.
  • character_id (int): The character ID of the person you're reporting.
  • reason_hashes (Sequence[int]): See
  • reason_category_hashes (Sequence[int]): See
async def fetch_friends(self, access_token: str, /) -> Mapping[str, typing.Any]:
1547    async def fetch_friends(self, access_token: str, /) -> typedefs.JSONObject:
1548        resp = await self._request(
1549            _GET,
1550            "Social/Friends/",
1551            auth=access_token,
1552        )
1553        assert isinstance(resp, dict)
1554        return resp

Fetch bungie friend list.

This requests OAuth2: ReadUserData scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
Returns
async def fetch_friend_requests(self, access_token: str, /) -> Mapping[str, typing.Any]:
1556    async def fetch_friend_requests(self, access_token: str, /) -> typedefs.JSONObject:
1557        resp = await self._request(
1558            _GET,
1559            "Social/Friends/Requests",
1560            auth=access_token,
1561        )
1562        assert isinstance(resp, dict)
1563        return resp

Fetch pending bungie friend requests queue.

This requests OAuth2: ReadUserData scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
Returns
async def accept_friend_request(self, access_token: str, /, member_id: int) -> None:
1565    async def accept_friend_request(self, access_token: str, /, member_id: int) -> None:
1566        await self._request(
1567            _POST,
1568            f"Social/Friends/Requests/Accept/{member_id}",
1569            auth=access_token,
1570        )

Accepts a friend relationship with the target user. The user must be on your incoming friend request list.

This request requires OAuth2: BnetWrite scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • member_id (int): The member's id to accept.
async def send_friend_request(self, access_token: str, /, member_id: int) -> None:
1572    async def send_friend_request(self, access_token: str, /, member_id: int) -> None:
1573        await self._request(
1574            _POST,
1575            f"Social/Friends/Add/{member_id}",
1576            auth=access_token,
1577        )

Requests a friend relationship with the target user.

This request requires OAuth2: BnetWrite scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • member_id (int): The member's id to send the request to.
async def decline_friend_request(self, access_token: str, /, member_id: int) -> None:
1579    async def decline_friend_request(
1580        self, access_token: str, /, member_id: int
1581    ) -> None:
1582        await self._request(
1583            _POST,
1584            f"Social/Friends/Requests/Decline/{member_id}",
1585            auth=access_token,
1586        )

Decline a friend request with the target user. The user must be in your incoming friend request list.

This request requires OAuth2: BnetWrite scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • member_id (int): The member's id to decline.
async def remove_friend(self, access_token: str, /, member_id: int) -> None:
1588    async def remove_friend(self, access_token: str, /, member_id: int) -> None:
1589        await self._request(
1590            _POST,
1591            f"Social/Friends/Remove/{member_id}",
1592            auth=access_token,
1593        )

Removes a friend from your friend list. The user must be in your friend list.

This request requires OAuth2: BnetWrite scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • member_id (int): The member's id to remove.
async def remove_friend_request(self, access_token: str, /, member_id: int) -> None:
1595    async def remove_friend_request(self, access_token: str, /, member_id: int) -> None:
1596        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1597        await self._request(
1598            _POST,
1599            f"Social/Friends/Requests/Remove/{member_id}",
1600            auth=access_token,
1601        )

Removes a friend from your friend list requests. The user must be in your outgoing request list.

.. note : This request requires OAuth2: BnetWrite scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • member_id (int): The member's id to remove from the requested friend list.
async def approve_all_pending_group_users( self, access_token: str, /, group_id: int, message: str | None = None) -> None:
1603    async def approve_all_pending_group_users(
1604        self,
1605        access_token: str,
1606        /,
1607        group_id: int,
1608        message: str | None = None,
1609    ) -> None:
1610        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1611        await self._request(
1612            _POST,
1613            f"GroupV2/{group_id}/Members/ApproveAll",
1614            auth=access_token,
1615            json={"message": str(message)},
1616        )

Approve all pending users for the given group id.

This request requires OAuth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The given group id.
Other Parameters
  • message (aiobungie.UndefinedOr[str]): An optional message to send with the request. Default is UNDEFINED.
async def deny_all_pending_group_users( self, access_token: str, /, group_id: int, *, message: str | None = None) -> None:
1618    async def deny_all_pending_group_users(
1619        self,
1620        access_token: str,
1621        /,
1622        group_id: int,
1623        *,
1624        message: str | None = None,
1625    ) -> None:
1626        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1627        await self._request(
1628            _POST,
1629            f"GroupV2/{group_id}/Members/DenyAll",
1630            auth=access_token,
1631            json={"message": str(message)},
1632        )

Deny all pending users for the given group id.

This request requires OAuth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The given group id.
Other Parameters
  • message (aiobungie.UndefinedOr[str]): An optional message to send with the request. Default is UNDEFINED.
async def add_optional_conversation( self, access_token: str, /, group_id: int, *, name: str | None = None, security: Literal[0, 1] = 0) -> None:
1634    async def add_optional_conversation(
1635        self,
1636        access_token: str,
1637        /,
1638        group_id: int,
1639        *,
1640        name: str | None = None,
1641        security: typing.Literal[0, 1] = 0,
1642    ) -> None:
1643        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1644        payload = {"chatName": str(name), "chatSecurity": security}
1645        await self._request(
1646            _POST,
1647            f"GroupV2/{group_id}/OptionalConversations/Add",
1648            json=payload,
1649            auth=access_token,
1650        )

Add a new chat channel to a group.

This request requires OAuth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The given group id.
Other parameters

name: aiobungie.UndefinedOr[str] The chat name. Default to UNDEFINED security: typing.Literal[0, 1] The security level of the chat.

If provided and set to 0, It will be to `Group` only.
If provided and set to 1, It will be `Admins` only.
Default is `0`
async def edit_optional_conversation( self, access_token: str, /, group_id: int, conversation_id: int, *, name: str | None = None, security: Literal[0, 1] = 0, enable_chat: bool = False) -> None:
1652    async def edit_optional_conversation(
1653        self,
1654        access_token: str,
1655        /,
1656        group_id: int,
1657        conversation_id: int,
1658        *,
1659        name: str | None = None,
1660        security: typing.Literal[0, 1] = 0,
1661        enable_chat: bool = False,
1662    ) -> None:
1663        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1664        payload = {
1665            "chatEnabled": enable_chat,
1666            "chatName": str(name),
1667            "chatSecurity": security,
1668        }
1669        await self._request(
1670            _POST,
1671            f"GroupV2/{group_id}/OptionalConversations/Edit/{conversation_id}",
1672            json=payload,
1673            auth=access_token,
1674        )

Edit the settings of this chat channel.

This request requires OAuth2: AdminGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The given group id.
  • conversation_id (int): The conversation/chat id.
Other parameters

name: aiobungie.UndefinedOr[str] The new chat name. Default to UNDEFINED security: typing.Literal[0, 1] The new security level of the chat.

If provided and set to 0, It will be to `Group` only.
If provided and set to 1, It will be `Admins` only.
Default is `0`

enable_chat : bool Whether to enable chatting or not. If set to True then chatting will be enabled. Otherwise it will be disabled.

async def transfer_item( self, access_token: str, /, item_id: int, item_hash: int, character_id: int, member_type: aiobungie.MembershipType | int, *, stack_size: int = 1, vault: bool = False) -> None:
1676    async def transfer_item(
1677        self,
1678        access_token: str,
1679        /,
1680        item_id: int,
1681        item_hash: int,
1682        character_id: int,
1683        member_type: enums.MembershipType | int,
1684        *,
1685        stack_size: int = 1,
1686        vault: bool = False,
1687    ) -> None:
1688        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1689        payload = {
1690            "characterId": character_id,
1691            "membershipType": int(member_type),
1692            "itemId": item_id,
1693            "itemReferenceHash": item_hash,
1694            "stackSize": stack_size,
1695            "transferToVault": vault,
1696        }
1697        await self._request(
1698            _POST,
1699            "Destiny2/Actions/Items/TransferItem",
1700            json=payload,
1701            auth=access_token,
1702        )

Transfer an item from / to your vault.

Notes
  • This method requires OAuth2: MoveEquipDestinyItems scope.
  • This method requires both item id and hash.
Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • item_id (int): The item instance id you to transfer.
  • item_hash (int): The item hash.
  • character_id (int): The character id to transfer the item from/to.
  • member_type (aiobungie.aiobungie.MembershipType | int): The user membership type.
Other Parameters
  • stack_size (int): The item stack size.
  • vault (bool): Whether to transfer this item to your vault or not. Defaults to False.
async def pull_item( self, access_token: str, /, item_id: int, item_hash: int, character_id: int, member_type: aiobungie.MembershipType | int, *, stack_size: int = 1, vault: bool = False) -> None:
1704    async def pull_item(
1705        self,
1706        access_token: str,
1707        /,
1708        item_id: int,
1709        item_hash: int,
1710        character_id: int,
1711        member_type: enums.MembershipType | int,
1712        *,
1713        stack_size: int = 1,
1714        vault: bool = False,
1715    ) -> None:
1716        # <<inherited docstring from aiobungie.interfaces.rest.RESTInterface>>
1717        payload = {
1718            "characterId": character_id,
1719            "membershipType": int(member_type),
1720            "itemId": item_id,
1721            "itemReferenceHash": item_hash,
1722            "stackSize": stack_size,
1723        }
1724        await self._request(
1725            _POST,
1726            "Destiny2/Actions/Items/PullFromPostmaster",
1727            json=payload,
1728            auth=access_token,
1729        )
1730        if vault:
1731            await self.transfer_item(
1732                access_token,
1733                item_id=item_id,
1734                item_hash=item_hash,
1735                character_id=character_id,
1736                member_type=member_type,
1737                stack_size=stack_size,
1738                vault=True,
1739            )

pull an item from the postmaster.

Notes
  • This method requires OAuth2: MoveEquipDestinyItems scope.
  • This method requires both item id and hash.
Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • item_id (int): The item instance id to pull.
  • item_hash (int): The item hash.
  • character_id (int): The character id to pull the item to.
  • member_type (aiobungie.aiobungie.MembershipType | int): The user membership type.
Other Parameters
  • stack_size (int): The item stack size.
  • vault (bool): If True, an extra HTTP call will be performed to transfer this item to the vault, Defaults to False.
async def fetch_fireteams( self, activity_type: aiobungie.FireteamActivity | int, *, platform: aiobungie.FireteamPlatform | int = <FireteamPlatform.ANY: 0>, language: aiobungie.FireteamLanguage | str = <FireteamLanguage.ALL: >, date_range: aiobungie.FireteamDate | int = <FireteamDate.ALL: 0>, page: int = 0, slots_filter: int = 0) -> Mapping[str, typing.Any]:
1741    async def fetch_fireteams(
1742        self,
1743        activity_type: fireteams.FireteamActivity | int,
1744        *,
1745        platform: fireteams.FireteamPlatform | int = fireteams.FireteamPlatform.ANY,
1746        language: fireteams.FireteamLanguage | str = fireteams.FireteamLanguage.ALL,
1747        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1748        page: int = 0,
1749        slots_filter: int = 0,
1750    ) -> typedefs.JSONObject:
1751        resp = await self._request(
1752            _GET,
1753            f"Fireteam/Search/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{page}/?langFilter={str(language)}",  # noqa: E501 Line too long
1754        )
1755        assert isinstance(resp, dict)
1756        return resp

Fetch public Bungie fireteams with open slots.

Parameters
Other Parameters
Returns
async def fetch_available_clan_fireteams( self, access_token: str, group_id: int, activity_type: aiobungie.FireteamActivity | int, *, platform: aiobungie.FireteamPlatform | int, language: aiobungie.FireteamLanguage | str, date_range: aiobungie.FireteamDate | int = <FireteamDate.ALL: 0>, page: int = 0, public_only: bool = False, slots_filter: int = 0) -> Mapping[str, typing.Any]:
1758    async def fetch_available_clan_fireteams(
1759        self,
1760        access_token: str,
1761        group_id: int,
1762        activity_type: fireteams.FireteamActivity | int,
1763        *,
1764        platform: fireteams.FireteamPlatform | int,
1765        language: fireteams.FireteamLanguage | str,
1766        date_range: fireteams.FireteamDate | int = fireteams.FireteamDate.ALL,
1767        page: int = 0,
1768        public_only: bool = False,
1769        slots_filter: int = 0,
1770    ) -> typedefs.JSONObject:
1771        resp = await self._request(
1772            _GET,
1773            f"Fireteam/Clan/{group_id}/Available/{int(platform)}/{int(activity_type)}/{int(date_range)}/{slots_filter}/{public_only}/{page}",  # noqa: E501
1774            json={"langFilter": str(language)},
1775            auth=access_token,
1776        )
1777        assert isinstance(resp, dict)
1778        return resp

Fetch a clan's fireteams with open slots.

This method requires OAuth2: ReadGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group/clan id of the fireteam.
  • activity_type (aiobungie.aiobungie.crates.FireteamActivity | int): The fireteam activity type.
Other Parameters
Returns
async def fetch_clan_fireteam( self, access_token: str, fireteam_id: int, group_id: int) -> Mapping[str, typing.Any]:
1780    async def fetch_clan_fireteam(
1781        self, access_token: str, fireteam_id: int, group_id: int
1782    ) -> typedefs.JSONObject:
1783        resp = await self._request(
1784            _GET,
1785            f"Fireteam/Clan/{group_id}/Summary/{fireteam_id}",
1786            auth=access_token,
1787        )
1788        assert isinstance(resp, dict)
1789        return resp

Fetch a specific clan fireteam.

This method requires OAuth2: ReadGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group/clan id to fetch the fireteam from.
  • fireteam_id (int): The fireteam id to fetch.
Returns
async def fetch_my_clan_fireteams( self, access_token: str, group_id: int, *, include_closed: bool = True, platform: aiobungie.FireteamPlatform | int, language: aiobungie.FireteamLanguage | str, filtered: bool = True, page: int = 0) -> Mapping[str, typing.Any]:
1791    async def fetch_my_clan_fireteams(
1792        self,
1793        access_token: str,
1794        group_id: int,
1795        *,
1796        include_closed: bool = True,
1797        platform: fireteams.FireteamPlatform | int,
1798        language: fireteams.FireteamLanguage | str,
1799        filtered: bool = True,
1800        page: int = 0,
1801    ) -> typedefs.JSONObject:
1802        payload = {"groupFilter": filtered, "langFilter": str(language)}
1803
1804        resp = await self._request(
1805            _GET,
1806            f"Fireteam/Clan/{group_id}/My/{int(platform)}/{include_closed}/{page}",
1807            json=payload,
1808            auth=access_token,
1809        )
1810        assert isinstance(resp, dict)
1811        return resp

Fetch a clan's fireteams with open slots.

This method requires OAuth2: ReadGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group/clan id to fetch.
Other Parameters
Returns
async def fetch_private_clan_fireteams(self, access_token: str, group_id: int, /) -> int:
1813    async def fetch_private_clan_fireteams(
1814        self, access_token: str, group_id: int, /
1815    ) -> int:
1816        resp = await self._request(
1817            _GET,
1818            f"Fireteam/Clan/{group_id}/ActiveCount",
1819            auth=access_token,
1820        )
1821        assert isinstance(resp, int)
1822        return resp

Fetch the active count of the clan fireteams that are only private.

This method requires OAuth2: ReadGroups scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • group_id (int): The group/clan id.
Returns
  • int: The active fireteams count. Max value returned is 25.
async def fetch_post_activity(self, instance_id: int, /) -> Mapping[str, typing.Any]:
1824    async def fetch_post_activity(self, instance_id: int, /) -> typedefs.JSONObject:
1825        resp = await self._request(
1826            _GET, f"Destiny2/Stats/PostGameCarnageReport/{instance_id}"
1827        )
1828        assert isinstance(resp, dict)
1829        return resp

Fetch a post activity details.

Parameters
  • instance_id (int): The activity instance id.
Returns
@helpers.unstable
async def search_entities( self, name: str, entity_type: str, *, page: int = 0) -> Mapping[str, typing.Any]:
1831    @helpers.unstable
1832    async def search_entities(
1833        self, name: str, entity_type: str, *, page: int = 0
1834    ) -> typedefs.JSONObject:
1835        resp = await self._request(
1836            _GET,
1837            f"Destiny2/Armory/Search/{entity_type}/{name}/",
1838            json={"page": page},
1839        )
1840        assert isinstance(resp, dict)
1841        return resp

Search for Destiny2 entities given a name and its type.

Parameters
  • name (str): The name of the entity, i.e., Thunderlord, One thousand voices.
  • entity_type (str): The type of the entity, AKA Definition, For an example DestinyInventoryItemDefinition
Other Parameters
  • page (int): An optional page to return. Default to 0.
Returns
async def fetch_unique_weapon_history( self, membership_id: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
1843    async def fetch_unique_weapon_history(
1844        self,
1845        membership_id: int,
1846        character_id: int,
1847        membership_type: enums.MembershipType | int,
1848    ) -> typedefs.JSONObject:
1849        resp = await self._request(
1850            _GET,
1851            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/UniqueWeapons/",
1852        )
1853        assert isinstance(resp, dict)
1854        return resp

Fetch details about unique weapon usage for a character. Includes all exotics.

Parameters
  • membership_id (int): The Destiny user membership id.
  • character_id (int): The character id to retrieve.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The Destiny user's membership type.
Returns
async def fetch_item( self, member_id: int, item_id: int, membership_type: aiobungie.MembershipType | int, components: Sequence[aiobungie.ComponentType]) -> Mapping[str, typing.Any]:
1856    async def fetch_item(
1857        self,
1858        member_id: int,
1859        item_id: int,
1860        membership_type: enums.MembershipType | int,
1861        components: collections.Sequence[enums.ComponentType],
1862    ) -> typedefs.JSONObject:
1863        collector = _collect_components(components)
1864
1865        resp = await self._request(
1866            _GET,
1867            f"Destiny2/{int(membership_type)}/Profile/{member_id}/Item/{item_id}/?components={collector}",
1868        )
1869        assert isinstance(resp, dict)
1870        return resp

Fetch an instanced Destiny 2 item's details.

Parameters
  • member_id (int): The membership id of the Destiny 2 player.
  • item_id (int): The instance id of the item.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The membership type of the Destiny 2 player.
  • components (collections.Sequence[aiobungie.ComponentType]): A list of components to retrieve.
Returns
async def fetch_clan_weekly_rewards(self, clan_id: int, /) -> Mapping[str, typing.Any]:
1872    async def fetch_clan_weekly_rewards(self, clan_id: int, /) -> typedefs.JSONObject:
1873        resp = await self._request(_GET, f"Destiny2/Clan/{clan_id}/WeeklyRewardState/")
1874        assert isinstance(resp, dict)
1875        return resp

Fetch the weekly reward state for a clan.

Parameters
  • clan_id (int): The clan id.
Returns
async def fetch_available_locales(self) -> Mapping[str, typing.Any]:
1877    async def fetch_available_locales(self) -> typedefs.JSONObject:
1878        resp = await self._request(_GET, "Destiny2/Manifest/DestinyLocaleDefinition/")
1879        assert isinstance(resp, dict)
1880        return resp

Fetch available locales at Bungie.

Returns
async def fetch_common_settings(self) -> Mapping[str, typing.Any]:
1882    async def fetch_common_settings(self) -> typedefs.JSONObject:
1883        resp = await self._request(_GET, "Settings")
1884        assert isinstance(resp, dict)
1885        return resp

Fetch the common settings used by Bungie's environment.

Returns
async def fetch_user_systems_overrides(self) -> Mapping[str, typing.Any]:
1887    async def fetch_user_systems_overrides(self) -> typedefs.JSONObject:
1888        resp = await self._request(_GET, "UserSystemOverrides")
1889        assert isinstance(resp, dict)
1890        return resp

Fetch a user's specific system overrides.

Returns
async def fetch_global_alerts( self, *, include_streaming: bool = False) -> Sequence[Mapping[str, typing.Any]]:
1892    async def fetch_global_alerts(
1893        self, *, include_streaming: bool = False
1894    ) -> typedefs.JSONArray:
1895        resp = await self._request(
1896            _GET, f"GlobalAlerts/?includestreaming={include_streaming}"
1897        )
1898        assert isinstance(resp, list)
1899        return resp

Fetch any active global alerts.

Parameters
  • include_streaming (bool): If True, the returned results will include streaming alerts. Default is False.
Returns
async def awainitialize_request( self, access_token: str, type: Literal[0, 1], membership_type: aiobungie.MembershipType | int, /, *, affected_item_id: int | None = None, character_id: int | None = None) -> Mapping[str, typing.Any]:
1901    async def awainitialize_request(
1902        self,
1903        access_token: str,
1904        type: typing.Literal[0, 1],
1905        membership_type: enums.MembershipType | int,
1906        /,
1907        *,
1908        affected_item_id: int | None = None,
1909        character_id: int | None = None,
1910    ) -> typedefs.JSONObject:
1911        body = {"type": type, "membershipType": int(membership_type)}
1912
1913        if affected_item_id is not None:
1914            body["affectedItemId"] = affected_item_id
1915
1916        if character_id is not None:
1917            body["characterId"] = character_id
1918
1919        resp = await self._request(
1920            _POST, "Destiny2/Awa/Initialize", json=body, auth=access_token
1921        )
1922        assert isinstance(resp, dict)
1923        return resp

Initialize a request to perform an advanced write action.

OAuth2: AdvancedWriteActions application scope is required to perform this request.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • type (typing.Literal[0, 1]): Type of the advanced write action. Its either 0 or 1. If set to 0 that means it None. Otherwise if 1 that means its insert plugs.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The Destiny membership type of the account to modify.
Other Parameters
  • affected_item_id (int | None): Item instance ID the action shall be applied to. This is optional for all but a new AwaType values.
  • character_id (int | None): The Destiny character ID to perform this action on.
Returns
async def awaget_action_token( self, access_token: str, correlation_id: str, /) -> Mapping[str, typing.Any]:
1925    async def awaget_action_token(
1926        self, access_token: str, correlation_id: str, /
1927    ) -> typedefs.JSONObject:
1928        resp = await self._request(
1929            _POST,
1930            f"Destiny2/Awa/GetActionToken/{correlation_id}",
1931            auth=access_token,
1932        )
1933        assert isinstance(resp, dict)
1934        return resp

Returns the action token if user approves the request.

OAuth2: AdvancedWriteActions application scope is required to perform this request.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • correlation_id (str): The identifier for the advanced write action request.
Returns
async def awa_provide_authorization_result( self, access_token: str, selection: int, correlation_id: str, nonce: MutableSequence[str | bytes]) -> int:
1936    async def awa_provide_authorization_result(
1937        self,
1938        access_token: str,
1939        selection: int,
1940        correlation_id: str,
1941        nonce: collections.MutableSequence[str | bytes],
1942    ) -> int:
1943        body = {"selection": selection, "correlationId": correlation_id, "nonce": nonce}
1944
1945        resp = await self._request(
1946            _POST,
1947            "Destiny2/Awa/AwaProvideAuthorizationResult",
1948            json=body,
1949            auth=access_token,
1950        )
1951        assert isinstance(resp, int)
1952        return resp

Provide the result of the user interaction. Called by the Bungie Destiny App to approve or reject a request.

OAuth2: AdvancedWriteActions application scope is required to perform this request.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • selection (int): Indication of the selection the user has made (Approving or rejecting the action)
  • correlation_id (str): Correlation ID of the request.
  • nonce (collections.MutableSequence[str | bytes]): Secret nonce received via the PUSH notification.
Returns
  • int: ...
async def fetch_vendors( self, access_token: str, character_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, /, components: Sequence[aiobungie.ComponentType], filter: int | None = None) -> Mapping[str, typing.Any]:
1954    async def fetch_vendors(
1955        self,
1956        access_token: str,
1957        character_id: int,
1958        membership_id: int,
1959        membership_type: enums.MembershipType | int,
1960        /,
1961        components: collections.Sequence[enums.ComponentType],
1962        filter: int | None = None,
1963    ) -> typedefs.JSONObject:
1964        components_ = _collect_components(components)
1965        route = (
1966            f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1967            f"/Character/{character_id}/Vendors/?components={components_}"
1968        )
1969
1970        if filter is not None:
1971            route = route + f"&filter={filter}"
1972
1973        resp = await self._request(
1974            _GET,
1975            route,
1976            auth=access_token,
1977        )
1978        assert isinstance(resp, dict)
1979        return resp

Get currently available vendors from the list of vendors that can possibly have rotating inventory.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • character_id (int): The character ID to return the vendor info for.
  • membership_id (int): The Destiny membership id to return the vendor info for.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The Destiny membership type to return the vendor info for.
  • components (collections.Sequence[aiobungie.ComponentType]): A list of vendor components to collect and return.
Other Parameters
  • filter (int): Filters the type of items returned from the vendor. This can be left to None.
Returns
async def fetch_vendor( self, access_token: str, character_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, vendor_hash: int, /, components: Sequence[aiobungie.ComponentType]) -> Mapping[str, typing.Any]:
1981    async def fetch_vendor(
1982        self,
1983        access_token: str,
1984        character_id: int,
1985        membership_id: int,
1986        membership_type: enums.MembershipType | int,
1987        vendor_hash: int,
1988        /,
1989        components: collections.Sequence[enums.ComponentType],
1990    ) -> typedefs.JSONObject:
1991        components_ = _collect_components(components)
1992        resp = await self._request(
1993            _GET,
1994            (
1995                f"Destiny2/{int(membership_type)}/Profile/{membership_id}"
1996                f"/Character/{character_id}/Vendors/{vendor_hash}/?components={components_}"
1997            ),
1998            auth=access_token,
1999        )
2000        assert isinstance(resp, dict)
2001        return resp

Fetch details for a specific vendor.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • character_id (int): The character ID to return the vendor info for.
  • membership_id (int): The Destiny membership id to return the vendor info for.
  • membership_type (aiobungie.aiobungie.MembershipType | int): The Destiny membership type to return the vendor info for.
  • vendor_hash (int): The vendor hash to return the details for.
  • components (collections.Sequence[aiobungie.ComponentType]): A list of vendor components to collect and return.
Returns
async def fetch_application_api_usage( self, access_token: str, application_id: int, /, *, start: datetime.datetime | None = None, end: datetime.datetime | None = None) -> Mapping[str, typing.Any]:
2003    async def fetch_application_api_usage(
2004        self,
2005        access_token: str,
2006        application_id: int,
2007        /,
2008        *,
2009        start: datetime.datetime | None = None,
2010        end: datetime.datetime | None = None,
2011    ) -> typedefs.JSONObject:
2012        end_date, start_date = time.parse_date_range(end, start)
2013        resp = await self._request(
2014            _GET,
2015            f"App/ApiUsage/{application_id}/?end={end_date}&start={start_date}",
2016            auth=access_token,
2017        )
2018        assert isinstance(resp, dict)
2019        return resp

Fetch a Bungie application's API usage.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • application_id (int): The application id to get.
Other Parameters
  • start (datetime.datetime | None): A datetime object can be used to collect the start of the application usage. This is limited and can go back to 30 days maximum.

    If this is left to None. It will return the last 24 hours.

  • end (datetime.datetime | None): A datetime object can be used to collect the end of the application usage.

    If this is left to None. It will return now.

Example
import datetime

# Fetch data from 2021 Dec 10th to 2021 Dec 20th
await fetch_application_api_usage(
    start=datetime.datetime(2021, 12, 10),
    end=datetime.datetime(2021, 12, 20)
)
Returns
async def fetch_bungie_applications(self) -> Sequence[Mapping[str, typing.Any]]:
2021    async def fetch_bungie_applications(self) -> typedefs.JSONArray:
2022        resp = await self._request(_GET, "App/FirstParty")
2023        assert isinstance(resp, list)
2024        return resp

Fetch details for applications created by Bungie.

Returns
async def fetch_content_type(self, type: str, /) -> Mapping[str, typing.Any]:
2026    async def fetch_content_type(self, type: str, /) -> typedefs.JSONObject:
2027        resp = await self._request(_GET, f"Content/GetContentType/{type}/")
2028        assert isinstance(resp, dict)
2029        return resp
async def fetch_content_by_id( self, id: int, locale: str, /, *, head: bool = False) -> Mapping[str, typing.Any]:
2031    async def fetch_content_by_id(
2032        self, id: int, locale: str, /, *, head: bool = False
2033    ) -> typedefs.JSONObject:
2034        resp = await self._request(
2035            _GET,
2036            f"Content/GetContentById/{id}/{locale}/",
2037            json={"head": head},
2038        )
2039        assert isinstance(resp, dict)
2040        return resp
async def fetch_content_by_tag_and_type( self, locale: str, tag: str, type: str, *, head: bool = False) -> Mapping[str, typing.Any]:
2042    async def fetch_content_by_tag_and_type(
2043        self, locale: str, tag: str, type: str, *, head: bool = False
2044    ) -> typedefs.JSONObject:
2045        resp = await self._request(
2046            _GET,
2047            f"Content/GetContentByTagAndType/{tag}/{type}/{locale}/",
2048            json={"head": head},
2049        )
2050        assert isinstance(resp, dict)
2051        return resp
async def search_content_with_text( self, locale: str, /, content_type: str, search_text: str, tag: str, *, page: int | None = None, source: str | None = None) -> Mapping[str, typing.Any]:
2053    async def search_content_with_text(
2054        self,
2055        locale: str,
2056        /,
2057        content_type: str,
2058        search_text: str,
2059        tag: str,
2060        *,
2061        page: int | None = None,
2062        source: str | None = None,
2063    ) -> typedefs.JSONObject:
2064        body: typedefs.JSONObject = {
2065            "locale": locale,
2066            "currentpage": page or 1,
2067            "ctype": content_type,
2068            "searchtxt": search_text,
2069            "searchtext": search_text,
2070            "tag": tag,
2071            "source": source,
2072        }
2073
2074        resp = await self._request(_GET, "Content/Search", params=body)
2075        assert isinstance(resp, dict)
2076        return resp
async def search_content_by_tag_and_type( self, locale: str, tag: str, type: str, *, page: int | None = None) -> Mapping[str, typing.Any]:
2078    async def search_content_by_tag_and_type(
2079        self,
2080        locale: str,
2081        tag: str,
2082        type: str,
2083        *,
2084        page: int | None = None,
2085    ) -> typedefs.JSONObject:
2086        body: typedefs.JSONObject = {"currentpage": page or 1}
2087
2088        resp = await self._request(
2089            _GET,
2090            f"Content/SearchContentByTagAndType/{tag}/{type}/{locale}/",
2091            params=body,
2092        )
2093        assert isinstance(resp, dict)
2094        return resp
async def search_help_articles(self, text: str, size: str, /) -> Mapping[str, typing.Any]:
2096    async def search_help_articles(
2097        self, text: str, size: str, /
2098    ) -> typedefs.JSONObject:
2099        resp = await self._request(_GET, f"Content/SearchHelpArticles/{text}/{size}/")
2100        assert isinstance(resp, dict)
2101        return resp
async def fetch_topics_page( self, category_filter: int, group: int, date_filter: int, sort: str | bytes, *, page: int | None = None, locales: Iterable[str] | None = None, tag_filter: str | None = None) -> Mapping[str, typing.Any]:
2103    async def fetch_topics_page(
2104        self,
2105        category_filter: int,
2106        group: int,
2107        date_filter: int,
2108        sort: str | bytes,
2109        *,
2110        page: int | None = None,
2111        locales: collections.Iterable[str] | None = None,
2112        tag_filter: str | None = None,
2113    ) -> typedefs.JSONObject:
2114        params = {
2115            "locales": ",".join(locales) if locales is not None else "en",
2116        }
2117        if tag_filter:
2118            params["tagstring"] = tag_filter
2119
2120        resp = await self._request(
2121            _GET,
2122            f"Forum/GetTopicsPaged/{page or 0}/0/{group}/{sort!s}/{date_filter}/{category_filter}/",
2123            params=params,
2124        )
2125        assert isinstance(resp, dict)
2126        return resp
async def fetch_core_topics_page( self, category_filter: int, date_filter: int, sort: str | bytes, *, page: int | None = None, locales: Iterable[str] | None = None) -> Mapping[str, typing.Any]:
2128    async def fetch_core_topics_page(
2129        self,
2130        category_filter: int,
2131        date_filter: int,
2132        sort: str | bytes,
2133        *,
2134        page: int | None = None,
2135        locales: collections.Iterable[str] | None = None,
2136    ) -> typedefs.JSONObject:
2137        resp = await self._request(
2138            _GET,
2139            f"Forum/GetCoreTopicsPaged/{page or 0}"
2140            f"/{sort!s}/{date_filter}/{category_filter}/?locales={','.join(locales) if locales else 'en'}",
2141        )
2142        assert isinstance(resp, dict)
2143        return resp
async def fetch_posts_threaded_page( self, parent_post: bool, page: int, page_size: int, parent_post_id: int, reply_size: int, root_thread_mode: bool, sort_mode: int, show_banned: str | None = None) -> Mapping[str, typing.Any]:
2145    async def fetch_posts_threaded_page(
2146        self,
2147        parent_post: bool,
2148        page: int,
2149        page_size: int,
2150        parent_post_id: int,
2151        reply_size: int,
2152        root_thread_mode: bool,
2153        sort_mode: int,
2154        show_banned: str | None = None,
2155    ) -> typedefs.JSONObject:
2156        resp = await self._request(
2157            _GET,
2158            f"Forum/GetPostsThreadedPaged/{parent_post}/{page}/"
2159            f"{page_size}/{reply_size}/{parent_post_id}/{root_thread_mode}/{sort_mode}/",
2160            json={"showbanned": show_banned},
2161        )
2162        assert isinstance(resp, dict)
2163        return resp
async def fetch_posts_threaded_page_from_child( self, child_id: bool, page: int, page_size: int, reply_size: int, root_thread_mode: bool, sort_mode: int, show_banned: str | None = None) -> Mapping[str, typing.Any]:
2165    async def fetch_posts_threaded_page_from_child(
2166        self,
2167        child_id: bool,
2168        page: int,
2169        page_size: int,
2170        reply_size: int,
2171        root_thread_mode: bool,
2172        sort_mode: int,
2173        show_banned: str | None = None,
2174    ) -> typedefs.JSONObject:
2175        resp = await self._request(
2176            _GET,
2177            f"Forum/GetPostsThreadedPagedFromChild/{child_id}/"
2178            f"{page}/{page_size}/{reply_size}/{root_thread_mode}/{sort_mode}/",
2179            json={"showbanned": show_banned},
2180        )
2181        assert isinstance(resp, dict)
2182        return resp
async def fetch_post_and_parent( self, child_id: int, /, *, show_banned: str | None = None) -> Mapping[str, typing.Any]:
2184    async def fetch_post_and_parent(
2185        self, child_id: int, /, *, show_banned: str | None = None
2186    ) -> typedefs.JSONObject:
2187        resp = await self._request(
2188            _GET,
2189            f"Forum/GetPostAndParent/{child_id}/",
2190            json={"showbanned": show_banned},
2191        )
2192        assert isinstance(resp, dict)
2193        return resp
async def fetch_posts_and_parent_awaiting( self, child_id: int, /, *, show_banned: str | None = None) -> Mapping[str, typing.Any]:
2195    async def fetch_posts_and_parent_awaiting(
2196        self, child_id: int, /, *, show_banned: str | None = None
2197    ) -> typedefs.JSONObject:
2198        resp = await self._request(
2199            _GET,
2200            f"Forum/GetPostAndParentAwaitingApproval/{child_id}/",
2201            json={"showbanned": show_banned},
2202        )
2203        assert isinstance(resp, dict)
2204        return resp
async def fetch_topic_for_content(self, content_id: int, /) -> int:
2206    async def fetch_topic_for_content(self, content_id: int, /) -> int:
2207        resp = await self._request(_GET, f"Forum/GetTopicForContent/{content_id}/")
2208        assert isinstance(resp, int)
2209        return resp
async def fetch_forum_tag_suggestions(self, partial_tag: str, /) -> Mapping[str, typing.Any]:
2211    async def fetch_forum_tag_suggestions(
2212        self, partial_tag: str, /
2213    ) -> typedefs.JSONObject:
2214        resp = await self._request(
2215            _GET,
2216            "Forum/GetForumTagSuggestions/",
2217            json={"partialtag": partial_tag},
2218        )
2219        assert isinstance(resp, dict)
2220        return resp
async def fetch_poll(self, topic_id: int, /) -> Mapping[str, typing.Any]:
2222    async def fetch_poll(self, topic_id: int, /) -> typedefs.JSONObject:
2223        resp = await self._request(_GET, f"Forum/Poll/{topic_id}/")
2224        assert isinstance(resp, dict)
2225        return resp
async def fetch_recruitment_thread_summaries(self) -> Sequence[Mapping[str, typing.Any]]:
2227    async def fetch_recruitment_thread_summaries(self) -> typedefs.JSONArray:
2228        resp = await self._request(_POST, "Forum/Recruit/Summaries/")
2229        assert isinstance(resp, list)
2230        return resp
async def fetch_available_avatars(self) -> Mapping[str, int]:
2248    async def fetch_available_avatars(self) -> collections.Mapping[str, int]:
2249        resp = await self._request(_GET, "GroupV2/GetAvailableAvatars/")
2250        assert isinstance(resp, dict)
2251        return resp
async def fetch_user_clan_invite_setting( self, access_token: str, /, membership_type: aiobungie.MembershipType | int) -> bool:
2253    async def fetch_user_clan_invite_setting(
2254        self,
2255        access_token: str,
2256        /,
2257        membership_type: enums.MembershipType | int,
2258    ) -> bool:
2259        resp = await self._request(
2260            _GET,
2261            f"GroupV2/GetUserClanInviteSetting/{int(membership_type)}/",
2262            auth=access_token,
2263        )
2264        assert isinstance(resp, bool)
2265        return resp
async def fetch_banned_group_members( self, access_token: str, group_id: int, /, *, page: int = 1) -> Mapping[str, typing.Any]:
2267    async def fetch_banned_group_members(
2268        self, access_token: str, group_id: int, /, *, page: int = 1
2269    ) -> typedefs.JSONObject:
2270        resp = await self._request(
2271            _GET,
2272            f"GroupV2/{group_id}/Banned/?currentpage={page}",
2273            auth=access_token,
2274        )
2275        assert isinstance(resp, dict)
2276        return resp
async def fetch_pending_group_memberships( self, access_token: str, group_id: int, /, *, current_page: int = 1) -> Mapping[str, typing.Any]:
2278    async def fetch_pending_group_memberships(
2279        self, access_token: str, group_id: int, /, *, current_page: int = 1
2280    ) -> typedefs.JSONObject:
2281        resp = await self._request(
2282            _GET,
2283            f"GroupV2/{group_id}/Members/Pending/?currentpage={current_page}",
2284            auth=access_token,
2285        )
2286        assert isinstance(resp, dict)
2287        return resp
async def fetch_invited_group_memberships( self, access_token: str, group_id: int, /, *, current_page: int = 1) -> Mapping[str, typing.Any]:
2289    async def fetch_invited_group_memberships(
2290        self, access_token: str, group_id: int, /, *, current_page: int = 1
2291    ) -> typedefs.JSONObject:
2292        resp = await self._request(
2293            _GET,
2294            f"GroupV2/{group_id}/Members/InvitedIndividuals/?currentpage={current_page}",
2295            auth=access_token,
2296        )
2297        assert isinstance(resp, dict)
2298        return resp
async def invite_member_to_group( self, access_token: str, /, group_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, *, message: str | None = None) -> Mapping[str, typing.Any]:
2300    async def invite_member_to_group(
2301        self,
2302        access_token: str,
2303        /,
2304        group_id: int,
2305        membership_id: int,
2306        membership_type: enums.MembershipType | int,
2307        *,
2308        message: str | None = None,
2309    ) -> typedefs.JSONObject:
2310        resp = await self._request(
2311            _POST,
2312            f"GroupV2/{group_id}/Members/IndividualInvite/{int(membership_type)}/{membership_id}/",
2313            auth=access_token,
2314            json={"message": str(message)},
2315        )
2316        assert isinstance(resp, dict)
2317        return resp
async def cancel_group_member_invite( self, access_token: str, /, group_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
2319    async def cancel_group_member_invite(
2320        self,
2321        access_token: str,
2322        /,
2323        group_id: int,
2324        membership_id: int,
2325        membership_type: enums.MembershipType | int,
2326    ) -> typedefs.JSONObject:
2327        resp = await self._request(
2328            _POST,
2329            f"GroupV2/{group_id}/Members/IndividualInviteCancel/{int(membership_type)}/{membership_id}/",
2330            auth=access_token,
2331        )
2332        assert isinstance(resp, dict)
2333        return resp
async def fetch_historical_definition(self) -> Mapping[str, typing.Any]:
2335    async def fetch_historical_definition(self) -> typedefs.JSONObject:
2336        resp = await self._request(_GET, "Destiny2/Stats/Definition/")
2337        assert isinstance(resp, dict)
2338        return resp
async def fetch_historical_stats( self, character_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, day_start: datetime.datetime, day_end: datetime.datetime, groups: Sequence[aiobungie.internal.enums.StatsGroupType | int], modes: Sequence[aiobungie.GameMode | int], *, period_type: aiobungie.internal.enums.PeriodType = <PeriodType.ALL_TIME: 2>) -> Mapping[str, typing.Any]:
2340    async def fetch_historical_stats(
2341        self,
2342        character_id: int,
2343        membership_id: int,
2344        membership_type: enums.MembershipType | int,
2345        day_start: datetime.datetime,
2346        day_end: datetime.datetime,
2347        groups: collections.Sequence[enums.StatsGroupType | int],
2348        modes: collections.Sequence[enums.GameMode | int],
2349        *,
2350        period_type: enums.PeriodType = enums.PeriodType.ALL_TIME,
2351    ) -> typedefs.JSONObject:
2352        end, start = time.parse_date_range(day_end, day_start)
2353        resp = await self._request(
2354            _GET,
2355            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Character/{character_id}/Stats/",
2356            json={
2357                "dayend": end,
2358                "daystart": start,
2359                "groups": [str(int(group)) for group in groups],
2360                "modes": [str(int(mode)) for mode in modes],
2361                "periodType": int(period_type),
2362            },
2363        )
2364        assert isinstance(resp, dict)
2365        return resp

Fetch historical stats for a specific membership character.

Parameters
  • character_id (int): The character ID to return the stats for.
  • membership_id (int): The Destiny membership id to return the stats for.
  • membership_type (aiobungie.MembershipType | int): The Destiny membership type to return the stats for.
  • day_start (datetime.datetime): The start of the day to return the stats for.
  • day_end (datetime.datetime): The end of the day to return the stats for.
  • groups (collections.Sequence[aiobungie.StatsGroupType]): A list of stats groups to return.
  • modes (collections.Sequence[aiobungie.GameMode | int]): A list of game modes to return.
  • period_type (aiobungie.enums.PeriodType): The period type to return the stats for. This will return ALL_TIME by default if not modified.
Returns
async def fetch_historical_stats_for_account( self, membership_id: int, membership_type: aiobungie.MembershipType | int, groups: Sequence[aiobungie.internal.enums.StatsGroupType | int]) -> Mapping[str, typing.Any]:
2367    async def fetch_historical_stats_for_account(
2368        self,
2369        membership_id: int,
2370        membership_type: enums.MembershipType | int,
2371        groups: collections.Sequence[enums.StatsGroupType | int],
2372    ) -> typedefs.JSONObject:
2373        resp = await self._request(
2374            _GET,
2375            f"Destiny2/{int(membership_type)}/Account/{membership_id}/Stats/",
2376            json={"groups": [str(int(group)) for group in groups]},
2377        )
2378        assert isinstance(resp, dict)
2379        return resp

Fetch historical stats for an account's membership.

Parameters
  • membership_id (int): The Destiny membership id to return the stats for.
  • membership_type (aiobungie.MembershipType | int): The Destiny membership type to return the stats for.
  • groups (collections.Sequence[aiobungie.StatsGroupType]): A list of stats groups to return.
Returns
async def fetch_aggregated_activity_stats( self, character_id: int, membership_id: int, membership_type: aiobungie.MembershipType | int, /) -> Mapping[str, typing.Any]:
2381    async def fetch_aggregated_activity_stats(
2382        self,
2383        character_id: int,
2384        membership_id: int,
2385        membership_type: enums.MembershipType | int,
2386        /,
2387    ) -> typedefs.JSONObject:
2388        resp = await self._request(
2389            _GET,
2390            f"Destiny2/{int(membership_type)}/Account/{membership_id}/"
2391            f"Character/{character_id}/Stats/AggregateActivityStats/",
2392        )
2393        assert isinstance(resp, dict)
2394        return resp

Fetch aggregated activity stats for a specific membership character.

Parameters
  • character_id (int): The character ID to return the stats for.
  • membership_id (int): The Destiny membership id to return the stats for.
  • membership_type (aiobungie.MembershipType | int): The Destiny membership type to return the stats for.
Returns
async def equip_loadout( self, access_token: str, /, loadout_index: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> None:
2396    async def equip_loadout(
2397        self,
2398        access_token: str,
2399        /,
2400        loadout_index: int,
2401        character_id: int,
2402        membership_type: enums.MembershipType | int,
2403    ) -> None:
2404        response = await self._request(
2405            _POST,
2406            "Destiny2/Actions/Loadouts/EquipLoadout/",
2407            json={
2408                "loadoutIndex": loadout_index,
2409                "characterId": character_id,
2410                "membership_type": int(membership_type),
2411            },
2412            auth=access_token,
2413        )
2414        assert isinstance(response, int)

Equip a loadout. Your character must be in a Social space, Orbit or Offline while performing this operation.

This operation requires MoveEquipDestinyItems OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • loadout_index (int): The index of the loadout to use.
  • character_id (int): The character ID to equip the loadout to.
  • membership_type (aiobungie.MembershipType | int): The membership type of the account.
async def snapshot_loadout( self, access_token: str, /, loadout_index: int, character_id: int, membership_type: aiobungie.MembershipType | int, *, color_hash: int | None = None, icon_hash: int | None = None, name_hash: int | None = None) -> None:
2416    async def snapshot_loadout(
2417        self,
2418        access_token: str,
2419        /,
2420        loadout_index: int,
2421        character_id: int,
2422        membership_type: enums.MembershipType | int,
2423        *,
2424        color_hash: int | None = None,
2425        icon_hash: int | None = None,
2426        name_hash: int | None = None,
2427    ) -> None:
2428        response = await self._request(
2429            _POST,
2430            "Destiny2/Actions/Loadouts/SnapshotLoadout/",
2431            auth=access_token,
2432            json={
2433                "colorHash": color_hash,
2434                "iconHash": icon_hash,
2435                "nameHash": name_hash,
2436                "loadoutIndex": loadout_index,
2437                "characterId": character_id,
2438                "membershipType": int(membership_type),
2439            },
2440        )
2441        assert isinstance(response, int)

Snapshot a loadout with the currently equipped items.

This operation requires MoveEquipDestinyItems OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • loadout_index (int): The index of the loadout to use.
  • character_id (int): The character ID to equip the loadout to.
  • membership_type (aiobungie.MembershipType | int): The membership type of the account.
Other Parameters
  • color_hash (int | None): ...
  • icon_hash (int | None): ...
  • name_hash (int | None): ...
async def update_loadout( self, access_token: str, /, loadout_index: int, character_id: int, membership_type: aiobungie.MembershipType | int, *, color_hash: int | None = None, icon_hash: int | None = None, name_hash: int | None = None) -> None:
2443    async def update_loadout(
2444        self,
2445        access_token: str,
2446        /,
2447        loadout_index: int,
2448        character_id: int,
2449        membership_type: enums.MembershipType | int,
2450        *,
2451        color_hash: int | None = None,
2452        icon_hash: int | None = None,
2453        name_hash: int | None = None,
2454    ) -> None:
2455        response = await self._request(
2456            _POST,
2457            "Destiny2/Actions/Loadouts/UpdateLoadoutIdentifiers/",
2458            auth=access_token,
2459            json={
2460                "colorHash": color_hash,
2461                "iconHash": icon_hash,
2462                "nameHash": name_hash,
2463                "loadoutIndex": loadout_index,
2464                "characterId": character_id,
2465                "membershipType": int(membership_type),
2466            },
2467        )
2468        assert isinstance(response, int)

Update the loadout. Color, Icon and Name.

This operation requires MoveEquipDestinyItems OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • loadout_index (int): The index of the loadout to use.
  • character_id (int): The character ID to equip the loadout to.
  • membership_type (aiobungie.MembershipType | int): The membership type of the account.
Other Parameters
  • color_hash (int | None): The new color hash of the loadout to update.
  • icon_hash (int | None): The new icon hash of the loadout to update.
  • name_hash (int | None): The new name hash of the loadout to update.
async def clear_loadout( self, access_token: str, /, loadout_index: int, character_id: int, membership_type: aiobungie.MembershipType | int) -> None:
2470    async def clear_loadout(
2471        self,
2472        access_token: str,
2473        /,
2474        loadout_index: int,
2475        character_id: int,
2476        membership_type: enums.MembershipType | int,
2477    ) -> None:
2478        response = await self._request(
2479            _POST,
2480            "Destiny2/Actions/Loadouts/ClearLoadout/",
2481            json={
2482                "loadoutIndex": loadout_index,
2483                "characterId": character_id,
2484                "membership_type": int(membership_type),
2485            },
2486            auth=access_token,
2487        )
2488        assert isinstance(response, int)

Clear the identifiers and items of a loadout.

This operation requires MoveEquipDestinyItems OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account.
  • loadout_index (int): The index of the loadout to use.
  • character_id (int): The character ID to equip the loadout to.
  • membership_type (aiobungie.MembershipType | int): The membership type of the account.
async def force_drops_repair(self, access_token: str, /) -> bool:
2490    async def force_drops_repair(self, access_token: str, /) -> bool:
2491        response = await self._request(
2492            _POST, "Tokens/Partner/ForceDropsRepair/", auth=access_token
2493        )
2494        assert isinstance(response, bool)
2495        return response

Twitch Drops self-repair function - scans twitch for drops not marked as fulfilled and resyncs them.

This operation requires PartnerOfferGrant OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account that will be used to make the request with.
Returns
  • bool: The nature of this response is a boolean.
async def claim_partner_offer( self, access_token: str, /, *, offer_id: str, bungie_membership_id: int, transaction_id: str) -> bool:
2497    async def claim_partner_offer(
2498        self,
2499        access_token: str,
2500        /,
2501        *,
2502        offer_id: str,
2503        bungie_membership_id: int,
2504        transaction_id: str,
2505    ) -> bool:
2506        response = await self._request(
2507            _POST,
2508            "Tokens/Partner/ClaimOffer/",
2509            json={
2510                "PartnerOfferId": offer_id,
2511                "BungieNetMembershipId": bungie_membership_id,
2512                "TransactionId": transaction_id,
2513            },
2514            auth=access_token,
2515        )
2516        assert isinstance(response, bool)
2517        return response

Claim a partner offer as the authenticated user.

This operation requires PartnerOfferGrant OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account that will be used to make the request with.
  • offer_id (str): The partner offer ID
  • bungie_membership_id (int): The associated Bungie.net membership ID
  • transaction_id (str): The transaction ID
Returns
  • bool: The nature of this response is a boolean.
async def fetch_bungie_rewards_for_user( self, access_token: str, /, membership_id: int) -> Mapping[str, typing.Any]:
2519    async def fetch_bungie_rewards_for_user(
2520        self, access_token: str, /, membership_id: int
2521    ) -> typedefs.JSONObject:
2522        response = await self._request(
2523            _GET,
2524            f"Tokens/Rewards/GetRewardsForUser/{membership_id}/",
2525            auth=access_token,
2526        )
2527        assert isinstance(response, dict)
2528        return response

Returns the bungie rewards for the targeted user.

This operation requires ReadAndApplyTokens OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account that will be used to make the request with.
  • membership_id (int): The associated membership ID to fetch the rewards for.
Returns
async def fetch_bungie_rewards_for_platform( self, access_token: str, /, membership_id: int, membership_type: aiobungie.MembershipType | int) -> Mapping[str, typing.Any]:
2530    async def fetch_bungie_rewards_for_platform(
2531        self,
2532        access_token: str,
2533        /,
2534        membership_id: int,
2535        membership_type: enums.MembershipType | int,
2536    ) -> typedefs.JSONObject:
2537        response = await self._request(
2538            _GET,
2539            f"Tokens/Rewards/GetRewardsForPlatformUser/{membership_id}/{int(membership_type)}",
2540            auth=access_token,
2541        )
2542        assert isinstance(response, dict)
2543        return response

Returns the bungie rewards for the targeted user and membership.

This operation requires ReadAndApplyTokens OAuth2 scope.

Parameters
  • access_token (str): The bearer access token associated with the bungie account that will be used to make the request with.
  • membership_id (int): The associated membership ID to fetch the rewards for.
  • membership_type (aiobungie.MembershipType | int): The associated membership type for the user.
Returns
async def fetch_bungie_rewards(self) -> Mapping[str, typing.Any]:
2545    async def fetch_bungie_rewards(self) -> typedefs.JSONObject:
2546        response = await self._request(_GET, "Tokens/Rewards/BungieRewards/")
2547        assert isinstance(response, dict)
2548        return response

Returns a list of the current bungie rewards.

Returns
async def fetch_fireteam_listing(self, listing_id: int) -> Mapping[str, typing.Any]:
2550    async def fetch_fireteam_listing(self, listing_id: int) -> typedefs.JSONObject:
2551        response = await self._request(_GET, f"FireteamFinder/Listing/{listing_id}/")
2552        assert isinstance(response, dict)
2553        return response
class RESTPool:
204class RESTPool:
205    """a Pool of `RESTClient` instances that shares the same TCP client connection.
206
207    This allows you to acquire instances of `RESTClient`s from single settings and credentials.
208
209    Example
210    -------
211    ```py
212    import aiobungie
213    import asyncio
214
215    pool = aiobungie.RESTPool("token")
216
217    async def get() -> None:
218        await pool.start()
219
220        async with pool.acquire() as client:
221            await client.fetch_character(...)
222
223        await pool.stop()
224
225    asyncio.run(get())
226    ```
227
228    Parameters
229    ----------
230    token : `str`
231        A valid application token from Bungie's developer portal.
232
233    Other Parameters
234    ----------------
235    client_secret : `str | None`
236        An optional application client secret,
237        This is only needed if you're fetching OAuth2 tokens with this client.
238    client_id : `int | None`
239        An optional application client id,
240        This is only needed if you're fetching OAuth2 tokens with this client.
241    settings: `aiobungie.builders.Settings | None`
242        The client settings to use, if `None` the default will be used.
243    max_retries : `int`
244        The max retries number to retry if the request hit a `5xx` status code.
245    debug : `bool | str`
246        Whether to enable logging responses or not.
247
248    Logging Levels
249    --------------
250    * `False`: This will disable logging.
251    * `True`: This will set the level to `DEBUG` and enable logging minimal information.
252    Like the response status, route, taken time and so on.
253    * `"TRACE" | aiobungie.TRACE`: This will log the response headers along with the minimal information.
254    """
255
256    __slots__ = (
257        "_token",
258        "_max_retries",
259        "_client_secret",
260        "_client_id",
261        "_metadata",
262        "_enable_debug",
263        "_client_session",
264        "_loads",
265        "_dumps",
266        "_settings",
267    )
268
269    # Looks like mypy doesn't like this.
270    if typing.TYPE_CHECKING:
271        _enable_debug: typing.Literal["TRACE"] | bool | int
272
273    def __init__(
274        self,
275        token: str,
276        /,
277        *,
278        client_secret: str | None = None,
279        client_id: int | None = None,
280        settings: builders.Settings | None = None,
281        dumps: typedefs.Dumps = helpers.dumps,
282        loads: typedefs.Loads = helpers.loads,
283        max_retries: int = 4,
284        debug: typing.Literal["TRACE"] | bool | int = False,
285    ) -> None:
286        self._client_secret = client_secret
287        self._client_id = client_id
288        self._token = token
289        self._max_retries = max_retries
290        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
291        self._enable_debug = debug
292        self._client_session: aiohttp.ClientSession | None = None
293        self._loads = loads
294        self._dumps = dumps
295        self._settings = settings or builders.Settings()
296
297    @property
298    def client_id(self) -> int | None:
299        """Return the client id of this REST client if provided, Otherwise None."""
300        return self._client_id
301
302    @property
303    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
304        """A general-purpose mutable mapping you can use to store data.
305
306        This mapping can be accessed from any process that has a reference to this pool.
307        """
308        return self._metadata
309
310    @property
311    def settings(self) -> builders.Settings:
312        """Internal client settings used within the HTTP client session."""
313        return self._settings
314
315    @typing.overload
316    def build_oauth2_url(self, client_id: int) -> builders.OAuthURL: ...
317
318    @typing.overload
319    def build_oauth2_url(self) -> builders.OAuthURL | None: ...
320
321    @typing.final
322    def build_oauth2_url(
323        self, client_id: int | None = None
324    ) -> builders.OAuthURL | None:
325        """Construct a new `OAuthURL` url object.
326
327        You can get the complete string representation of the url by calling `.compile()` on it.
328
329        Parameters
330        ----------
331        client_id : `int | None`
332            An optional client id to provide, If left `None` it will roll back to the id passed
333            to the `RESTClient`, If both is `None` this method will return `None`.
334
335        Returns
336        -------
337        `aiobungie.builders.OAuthURL | None`
338            * If `client_id` was provided as a parameter, It guarantees to return a complete `OAuthURL` object
339            * If `client_id` is set to `aiobungie.RESTClient` will be.
340            * If both are `None` this method will return `None.
341        """
342        client_id = client_id or self._client_id
343        if client_id is None:
344            return None
345
346        return builders.OAuthURL(client_id=client_id)
347
348    async def start(self) -> None:
349        """Start the TCP connection of this client pool.
350
351        This will raise `RuntimeError` if the connection has already been started.
352
353        Example
354        -------
355        ```py
356        pool = aiobungie.RESTPool(...)
357
358        async def run() -> None:
359            await pool.start()
360            async with pool.acquire() as client:
361                # use client
362
363        async def stop(self) -> None:
364            await pool.close()
365        ```
366        """
367        if self._client_session is not None:
368            raise RuntimeError("<RESTPool> has already been started.") from None
369
370        self._client_session = aiohttp.ClientSession(
371            connector=aiohttp.TCPConnector(
372                use_dns_cache=self._settings.use_dns_cache,
373                ttl_dns_cache=self._settings.ttl_dns_cache,
374                ssl_context=self._settings.ssl_context,
375                ssl=self._settings.ssl,
376            ),
377            connector_owner=True,
378            raise_for_status=False,
379            timeout=self._settings.http_timeout,
380            trust_env=self._settings.trust_env,
381            headers=self._settings.headers,
382        )
383
384    async def stop(self) -> None:
385        """Stop the TCP connection of this client pool.
386
387        This will raise `RuntimeError` if the connection has already been closed.
388
389        Example
390        -------
391        ```py
392        pool = aiobungie.RESTPool(...)
393
394        async def run() -> None:
395            await pool.start()
396            async with pool.acquire() as client:
397                # use client
398
399        async def stop(self) -> None:
400            await pool.close()
401        ```
402        """
403        if self._client_session is None:
404            raise RuntimeError("<RESTPool> is already stopped.")
405
406        await self._client_session.close()
407        self._client_session = None
408
409    @typing.final
410    def acquire(self) -> RESTClient:
411        """Acquires a new `RESTClient` instance from this pool.
412
413        Returns
414        -------
415        `RESTClient`
416            An instance of a `RESTClient`.
417        """
418        return RESTClient(
419            self._token,
420            client_secret=self._client_secret,
421            client_id=self._client_id,
422            loads=self._loads,
423            dumps=self._dumps,
424            max_retries=self._max_retries,
425            debug=self._enable_debug,
426            client_session=self._client_session,
427            owned_client=False,
428            settings=self._settings,
429        )

a Pool of RESTClient instances that shares the same TCP client connection.

This allows you to acquire instances of RESTClients from single settings and credentials.

Example
import aiobungie
import asyncio

pool = aiobungie.RESTPool("token")

async def get() -> None:
    await pool.start()

    async with pool.acquire() as client:
        await client.fetch_character(...)

    await pool.stop()

asyncio.run(get())
Parameters
  • token (str): A valid application token from Bungie's developer portal.
Other Parameters
  • client_secret (str | None): An optional application client secret, This is only needed if you're fetching OAuth2 tokens with this client.
  • client_id (int | None): An optional application client id, This is only needed if you're fetching OAuth2 tokens with this client.
  • settings (aiobungie.builders.Settings | None): The client settings to use, if None the default will be used.
  • max_retries (int): The max retries number to retry if the request hit a 5xx status code.
  • debug (bool | str): Whether to enable logging responses or not.
Logging Levels
  • False: This will disable logging.
  • True: This will set the level to DEBUG and enable logging minimal information. Like the response status, route, taken time and so on.
  • "TRACE" | aiobungie.TRACE: This will log the response headers along with the minimal information.
RESTPool( token: str, /, *, client_secret: str | None = None, client_id: int | None = None, settings: aiobungie.builders.Settings | None = None, dumps: Callable[[Mapping[str, typing.Any] | Sequence[Mapping[str, typing.Any]]], bytes] = <function dumps>, loads: Callable[[str | bytes], Sequence[Mapping[str, typing.Any]] | Mapping[str, typing.Any]] = <function loads>, max_retries: int = 4, debug: Union[Literal['TRACE'], bool, int] = False)
273    def __init__(
274        self,
275        token: str,
276        /,
277        *,
278        client_secret: str | None = None,
279        client_id: int | None = None,
280        settings: builders.Settings | None = None,
281        dumps: typedefs.Dumps = helpers.dumps,
282        loads: typedefs.Loads = helpers.loads,
283        max_retries: int = 4,
284        debug: typing.Literal["TRACE"] | bool | int = False,
285    ) -> None:
286        self._client_secret = client_secret
287        self._client_id = client_id
288        self._token = token
289        self._max_retries = max_retries
290        self._metadata: collections.MutableMapping[typing.Any, typing.Any] = {}
291        self._enable_debug = debug
292        self._client_session: aiohttp.ClientSession | None = None
293        self._loads = loads
294        self._dumps = dumps
295        self._settings = settings or builders.Settings()
client_id: int | None
297    @property
298    def client_id(self) -> int | None:
299        """Return the client id of this REST client if provided, Otherwise None."""
300        return self._client_id

Return the client id of this REST client if provided, Otherwise None.

metadata: MutableMapping[typing.Any, typing.Any]
302    @property
303    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
304        """A general-purpose mutable mapping you can use to store data.
305
306        This mapping can be accessed from any process that has a reference to this pool.
307        """
308        return self._metadata

A general-purpose mutable mapping you can use to store data.

This mapping can be accessed from any process that has a reference to this pool.

settings: aiobungie.builders.Settings
310    @property
311    def settings(self) -> builders.Settings:
312        """Internal client settings used within the HTTP client session."""
313        return self._settings

Internal client settings used within the HTTP client session.

@typing.final
def build_oauth2_url(self, client_id: int | None = None) -> aiobungie.builders.OAuthURL | None:
321    @typing.final
322    def build_oauth2_url(
323        self, client_id: int | None = None
324    ) -> builders.OAuthURL | None:
325        """Construct a new `OAuthURL` url object.
326
327        You can get the complete string representation of the url by calling `.compile()` on it.
328
329        Parameters
330        ----------
331        client_id : `int | None`
332            An optional client id to provide, If left `None` it will roll back to the id passed
333            to the `RESTClient`, If both is `None` this method will return `None`.
334
335        Returns
336        -------
337        `aiobungie.builders.OAuthURL | None`
338            * If `client_id` was provided as a parameter, It guarantees to return a complete `OAuthURL` object
339            * If `client_id` is set to `aiobungie.RESTClient` will be.
340            * If both are `None` this method will return `None.
341        """
342        client_id = client_id or self._client_id
343        if client_id is None:
344            return None
345
346        return builders.OAuthURL(client_id=client_id)

Construct a new OAuthURL url object.

You can get the complete string representation of the url by calling .compile() on it.

Parameters
  • client_id (int | None): An optional client id to provide, If left None it will roll back to the id passed to the RESTClient, If both is None this method will return None.
Returns
async def start(self) -> None:
348    async def start(self) -> None:
349        """Start the TCP connection of this client pool.
350
351        This will raise `RuntimeError` if the connection has already been started.
352
353        Example
354        -------
355        ```py
356        pool = aiobungie.RESTPool(...)
357
358        async def run() -> None:
359            await pool.start()
360            async with pool.acquire() as client:
361                # use client
362
363        async def stop(self) -> None:
364            await pool.close()
365        ```
366        """
367        if self._client_session is not None:
368            raise RuntimeError("<RESTPool> has already been started.") from None
369
370        self._client_session = aiohttp.ClientSession(
371            connector=aiohttp.TCPConnector(
372                use_dns_cache=self._settings.use_dns_cache,
373                ttl_dns_cache=self._settings.ttl_dns_cache,
374                ssl_context=self._settings.ssl_context,
375                ssl=self._settings.ssl,
376            ),
377            connector_owner=True,
378            raise_for_status=False,
379            timeout=self._settings.http_timeout,
380            trust_env=self._settings.trust_env,
381            headers=self._settings.headers,
382        )

Start the TCP connection of this client pool.

This will raise RuntimeError if the connection has already been started.

Example
pool = aiobungie.RESTPool(...)

async def run() -> None:
    await pool.start()
    async with pool.acquire() as client:
        # use client

async def stop(self) -> None:
    await pool.close()
async def stop(self) -> None:
384    async def stop(self) -> None:
385        """Stop the TCP connection of this client pool.
386
387        This will raise `RuntimeError` if the connection has already been closed.
388
389        Example
390        -------
391        ```py
392        pool = aiobungie.RESTPool(...)
393
394        async def run() -> None:
395            await pool.start()
396            async with pool.acquire() as client:
397                # use client
398
399        async def stop(self) -> None:
400            await pool.close()
401        ```
402        """
403        if self._client_session is None:
404            raise RuntimeError("<RESTPool> is already stopped.")
405
406        await self._client_session.close()
407        self._client_session = None

Stop the TCP connection of this client pool.

This will raise RuntimeError if the connection has already been closed.

Example
pool = aiobungie.RESTPool(...)

async def run() -> None:
    await pool.start()
    async with pool.acquire() as client:
        # use client

async def stop(self) -> None:
    await pool.close()
@typing.final
def acquire(self) -> RESTClient:
409    @typing.final
410    def acquire(self) -> RESTClient:
411        """Acquires a new `RESTClient` instance from this pool.
412
413        Returns
414        -------
415        `RESTClient`
416            An instance of a `RESTClient`.
417        """
418        return RESTClient(
419            self._token,
420            client_secret=self._client_secret,
421            client_id=self._client_id,
422            loads=self._loads,
423            dumps=self._dumps,
424            max_retries=self._max_retries,
425            debug=self._enable_debug,
426            client_session=self._client_session,
427            owned_client=False,
428            settings=self._settings,
429        )

Acquires a new RESTClient instance from this pool.

Returns
TRACE: Final[int] = 5

The trace logging level for the RESTClient responses.

You can enable this with the following code

>>> import logging
>>> logging.getLogger("aiobungie.rest").setLevel(aiobungie.TRACE)
<h1 id="or">or</h1>
>>> logging.basicConfig(level=aiobungie.TRACE)
<h1 id="or-2">Or</h1>
>>> client = aiobungie.RESTClient(debug="TRACE")
<h1 id="or-if-youre-using-aiobungieclient">Or if you're using <code>aiobungie.Client</code></h1>
>>> client = aiobungie.Client()
>>> client.rest.with_debug(level=aiobungie.TRACE, file="rest_logs.txt")