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 ''}¤tpage=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
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 ''}¤tpage=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, ifNone
the default will be used. - owned_client (
bool
):- If set to
True
, this client will use the providedclient_session
parameter instead, - If set to
True
andclient_session
isNone
,ValueError
will be raised. - If set to
False
, aiobungie will initialize a new client session for you.
- If set to
- client_session (
aiohttp.ClientSession | None
): If provided, this client session will be used to make all the HTTP requests. Theowned_client
must be set toTrue
for this to work. - max_retries (
int
): The max retries number to retry if the request hit a5xx
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 toDEBUG
and enable logging minimal information."TRACE" | aiobungie.TRACE
: This will log the response headers along with the minimal information.
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)
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)
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.
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.
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 beGET
,POST
,PUT
, etc. - path (
str
): The Bungie endpoint or path. A path must look something like thisDestiny2/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.
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 leftNone
it will roll back to the id passed to theRESTClient
, If both isNone
this method will returnNone
.
Returns
aiobungie.builders.OAuthURL | None
: * Ifclient_id
was provided as a parameter, It guarantees to return a completeOAuthURL
object- If
client_id
is set toaiobungie.RESTClient
will be. - If both are
None
this method will return `None.
- If
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
aiobungie.builders.OAuth2Response
: An OAuth2 deserialized response.
Raises
aiobungie.error.Unauthorized
: The passed code was invalid.
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
aiobungie.builders.OAuth2Response
: An OAuth2 deserialized response.
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
aiobungie.typedefs.JSONObject
: A JSON object of users objects.
Raises
aiobungie.error.NotFound
: The user was not found.
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
- id (
int
): The user's id. - type (
aiobungie.aiobungie.MembershipType | int
): The user's membership type.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the found user.
Raises
- aiobungie.NotFound: The requested user was not found.
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
aiobungie.typedefs.JSONArray
: A JSON array of the found player's memberships.
Raises
aiobungie.NotFound
: The player was not found.aiobungie.MembershipTypeError
: The provided membership type was invalid.
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
aiobungie.typedefs.JSONObject
: A JSON object contains all the available display names.
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
aiobungie.typedefs.JSONObject
: A JSON object of an array of the found users.
Raises
aiobungie.NotFound
: The user(s) was not found.
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
aiobungie.typedefs.JSONObject
: A JSON object of the clan.
Raises
aiobungie.NotFound
: The clan was not found.
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
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.- type (
aiobungie.aiobungie.GroupType | int
): The group type, Default is one.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the clan.
Raises
aiobungie.NotFound
: The clan was not found.
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 to0
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
aiobungie.typedefs.JSONObject
: A JSON object of the search results.
Raises
ValueError
: If the group type isaiobungie.GroupType.CLAN
andgroup_member_count_filter
,locale_filter
andtag_text
are notNone
.
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
aiobungie.typedefs.JSONObject
: A JSON object of the clan admins and founder members.
Raises
aiobungie.NotFound
: The clan was not found.
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
aiobungie.typedefs.JSONArray
: A JSON array of the conversations.
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
aiobungie.typedefs.JSONObject
: A JSON object of the application.
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
- member_id (
int
): A valid bungie member id. - membership_type (
aiobungie.aiobungie.internal.enums.MembershipType | int
): The member's membership type. - character_id (
int
): The character id to return. - components (
collections.Sequence[aiobungie.ComponentType]
): A list of character components to collect and return.
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
aiobungie.typedefs.JSONObject
: A JSON object of the requested character.
Raises
aiobungie.MembershipTypeError
: The provided membership type was invalid.
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 with4611
. - 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
- membership_type (
aiobungie.aiobungie.internal.enums.MembershipType | int
): The Member ship type, if nothing was passed than it will return all. - page (
int
): The page number. Default to1
- limit (
int
): Limit the returned result. Default to1
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the player's activities.
Raises
aiobungie.error.NotFound
: The activity was not found.aiobungie.MembershipTypeError
: The provided membership type was invalid.
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
- membership_id (
int
): The member's id. - type (
aiobungie.aiobungie.MembershipType | int
): A valid membership type. - components (
collections.Sequence[aiobungie.ComponentType]
): A sequence of profile components to collect and return.
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
aiobungie.typedefs.JSONObject
: A JSON object of the found profile.
Raises
aiobungie.MembershipTypeError
: The provided membership type was invalid.
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
aiobungie.typedefs.JSONObject
: A JSON object of the definition data.
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
aiobungie.typedefs.JSONObject
: A JSON object of the inventory item.
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
aiobungie.typedefs.JSONObject
: A JSON object of the objective data.
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
- member_id (
int
): The member's id - member_type (
aiobungie.aiobungie.MembershipType | int
): The member's membership type.
Other Parameters
- filter (
int
): Filter apply to list of joined groups. This Default to0
- group_type (
aiobungie.aiobungie.GroupType | int
): The group's type. This is always set toaiobungie.GroupType.CLAN
and should not be changed.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of an array of the member's membership data and groups data.
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
- member_id (
int
): The member's id - member_type (
aiobungie.aiobungie.MembershipType | int
): The member's membership type.
Other Parameters
- filter (
int
): Filter apply to list of joined groups. This Default to0
- group_type (
aiobungie.aiobungie.GroupType | int
): The group's type. This is always set toaiobungie.GroupType.CLAN
and should not be changed.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of an array of the member's membership data and groups data.
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 ''}¤tpage=1", 1101 ) 1102 assert isinstance(resp, dict) 1103 return resp
Fetch all Bungie Clan members.
Parameters
- clan_id (
int
): The clans id
Other Parameters
- name (
str | None
): If provided, Only players matching this name will be returned. - type (
aiobungie.aiobungie.MembershipType | int
): An optional clan member's membership type. Default is set toaiobungie.MembershipType.NONE
Which returns the first matched clan member by their name.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of an array of clan members.
Raises
aiobungie.NotFound
: The clan was not found.
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
aiobungie.typedefs.JSONObject
: A JSON object of the found user hard linked types.
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
aiobungie.typedefs.JSONArray
: A JSON array of the returned credentials.
Raises
aiobungie.Unauthorized
: The access token was wrong or no access token passed.
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
aiobungie.typedefs.JSONObject
: A JSON object contains the changed item details.
Raises
aiobungie.Unauthorized
: The access token was wrong or no access token passed.
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
aiobungie.typedefs.JSONObject
: A JSON object contains the changed item details.
Raises
aiobungie.Unauthorized
: The access token was wrong or no access token passed.
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
): IfTrue
, The item will be locked, IfFalse
, 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
aiobungie.Unauthorized
: - The access token was wrong- No access token passed.
- Other authorization causes.
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
): IfTrue
, The item will be locked, IfFalse
, 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
aiobungie.Unauthorized
: - The access token was wrong- No access token passed.
- Other authorization causes.
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.
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.
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 ismanifest
- force (
bool
): Whether to force the download. Default isFalse
. 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 andforce
isFalse
.ValueError
: If the provided language was not recognized.
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 ismanifest
. - 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.
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.
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 toTrue
, 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 isFalse
.
Returns
aiobungie.typedefs.JSONObject
- A JSON object which contains an Array of profiles, an Array of profiles with errors and Bungie.Net membership
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
aiobungie.typedefs.JSONObject
: A JSON object of information about the milestones.
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
aiobungie.typedefs.JSONObject
: A JSON object of information related to the fetched milestone.
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
aiobungie.typedefs.JSONObject
: A JSON object of the bungie net user and destiny memberships of this account.
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.
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.
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 isUNDEFINED
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.
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
aiobungie.typedefs.JSONObject
: A JSON object of the group that the member has been kicked from.
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 includeaccess_token
andgroup_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 toTrue
, The clan will set to private. If provided and set toFalse
, 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 toTrue
, The clan members will be allowed to chat. If provided and set toFalse
, The clan members will not be allowed to chat. - chat_security (
aiobungie.typedefs.NoneOr[typing.Literal[0, 1]]
): If provided and set to0
, The clan chat security will be edited toGroup
only. If provided and set to1
, The clan chat security will be edited toAdmin
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 to0
, The clan chat homepage will be edited toWall
. If provided and set to1
, The clan chat homepage will be edited toForum
. If provided and set to0
, The clan chat homepage will be edited toAllianceForum
. - enable_invite_messaging_for_admins (
bool | None
): ??? - default_publicity (
aiobungie.typedefs.NoneOr[typing.Literal[0, 1, 2]]
): If provided and set to0
, The clan chat publicity will be edited toPublic
. If provided and set to1
, The clan chat publicity will be edited toAlliance
. If provided and set to2
, The clan chat publicity will be edited toPrivate
. - is_public_topic_admin (
bool | 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 includeaccess_token
andgroup_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
-> Beginner2
-> 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 isaiobungie.ClanMemberType.BEGINNER
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
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
aiobungie.typedefs.JSONObject
: A JSON object of an array of the bungie friends's data.
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
aiobungie.typedefs.JSONObject
: A JSON object of incoming requests and outgoing requests.
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.
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.
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.
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.
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.
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 isUNDEFINED
.
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 isUNDEFINED
.
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`
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.
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 toFalse
.
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
): IfTrue
, an extra HTTP call will be performed to transfer this item to the vault, Defaults toFalse
.
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
- activity_type (
aiobungie.aiobungie.crates.FireteamActivity | int
): The fireteam activity type.
Other Parameters
- platform (
aiobungie.aiobungie.crates.fireteams.FireteamPlatform | int
): If this is provided. Then the results will be filtered with the given platform. Defaults toaiobungie.crates.FireteamPlatform.ANY
which returns all platforms. - language (
aiobungie.crates.fireteams.FireteamLanguage | str
): A locale language to filter the used language in that fireteam. Defaults toaiobungie.crates.FireteamLanguage.ALL
- date_range (
aiobungie.aiobungie.FireteamDate | int
): An integer to filter the date range of the returned fireteams. Defaults toaiobungie.FireteamDate.ALL
. - page (
int
): The page number. By default its0
which returns all available activities. - slots_filter (
int
): Filter the returned fireteams based on available slots. Default is0
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the fireteam details.
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
- platform (
aiobungie.aiobungie.crates.fireteams.FireteamPlatform | int
): If this is provided. Then the results will be filtered with the given platform. Defaults toaiobungie.crates.FireteamPlatform.ANY
which returns all platforms. - language (
aiobungie.crates.fireteams.FireteamLanguage | str
): A locale language to filter the used language in that fireteam. Defaults toaiobungie.crates.FireteamLanguage.ALL
- date_range (
aiobungie.aiobungie.FireteamDate | int
): An integer to filter the date range of the returned fireteams. Defaults toaiobungie.FireteamDate.ALL
. - page (
int
): The page number. By default its0
which returns all available activities. - public_only (
bool
): If set to True, Then only public fireteams will be returned. - slots_filter (
int
): Filter the returned fireteams based on available slots. Default is0
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the fireteams detail.
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
aiobungie.typedefs.JSONObject
: A JSON object of the fireteam details.
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
- include_closed (
bool
): If provided and set toTrue
, It will also return closed fireteams. If provided and set toFalse
, It will only return public fireteams. Default isTrue
. - platform (
aiobungie.aiobungie.crates.fireteams.FireteamPlatform | int
): If this is provided. Then the results will be filtered with the given platform. Defaults toaiobungie.crates.FireteamPlatform.ANY
which returns all platforms. - language (
aiobungie.crates.fireteams.FireteamLanguage | str
): A locale language to filter the used language in that fireteam. Defaults toaiobungie.crates.FireteamLanguage.ALL
- filtered (
bool
): If set toTrue
, it will filter by clan. Otherwise not. Default isTrue
. - page (
int
): The page number. By default its0
which returns all available activities.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the fireteams detail.
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.
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
aiobungie.typedefs.JSONObject
: A JSON object of the post activity.
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 exampleDestinyInventoryItemDefinition
Other Parameters
- page (
int
): An optional page to return. Default to 0.
Returns
aiobungie.typedefs.JSONObject
: A JSON object contains details about the searched term.
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
aiobungie.typedefs.JSONObject
: A JSON object contains details about the returned weapons.
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
aiobungie.typedefs.JSONObject
: A JSON object response contains the fetched item with its components.
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
aiobungie.typedefs.JSONObject
: A JSON response of the clan rewards state.
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
aiobungie.typedefs.JSONObject
: A JSON object contains a list of all available localization cultures.
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
aiobungie.typedefs.JSONObject
: The common settings JSON object.
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
aiobungie.typedefs.JSONObject
: The system overrides JSON object.
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
aiobungie.typedefs.JSONArray
: A JSON array of the global alerts objects.
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 itNone
. 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
aiobungie.typedefs.JSONObject
: A JSON object response.
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
aiobungie.typedefs.JSONObject
: A JSON object response.
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 toNone
.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the vendor response.
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
aiobungie.typedefs.JSONObject
: A JSON object of the vendor response.
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 returnnow
.
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
aiobungie.typedefs.JSONObject
: A JSON object of the application usage details.
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
aiobungie.typedefs.JSONArray
: An array of Bungie created applications.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 returnALL_TIME
by default if not modified.
Returns
aiobungie.typedefs.JSONObject
: A JSON object of the historical stats.
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
aiobungie.typedefs.JSONObject
: A JSON object of the historical stats for the account. This includes both the character and account stats.
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
aiobungie.typedefs.JSONObject
: A JSON object of the aggregated activity stats.
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.
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
): ...
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.
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.
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 aboolean
.
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 aboolean
.
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
aiobungie.typedefs.JSONObject
: A JSON object contains a display of the rewards. See
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
aiobungie.typedefs.JSONObject
: A JSON object contains a display of the rewards. See
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
aiobungie.typedefs.JSONObject
: A JSON object contains a display of the rewards. See
Inherited Members
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 RESTClient
s 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, ifNone
the default will be used. - max_retries (
int
): The max retries number to retry if the request hit a5xx
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 toDEBUG
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.
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()
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.
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.
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.
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 leftNone
it will roll back to the id passed to theRESTClient
, If both isNone
this method will returnNone
.
Returns
aiobungie.builders.OAuthURL | None
: * Ifclient_id
was provided as a parameter, It guarantees to return a completeOAuthURL
object- If
client_id
is set toaiobungie.RESTClient
will be. - If both are
None
this method will return `None.
- If
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()
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()
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 )
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")