Adding all files
This commit is contained in:
542
.local/lib/python3.14/site-packages/disnake/asset.py
Normal file
542
.local/lib/python3.14/site-packages/disnake/asset.py
Normal file
@@ -0,0 +1,542 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, Union
|
||||
|
||||
import yarl
|
||||
|
||||
from . import utils
|
||||
from .errors import DiscordException
|
||||
from .file import File
|
||||
|
||||
__all__ = ("Asset",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .state import ConnectionState
|
||||
from .webhook.async_ import BaseWebhook, _WebhookState
|
||||
|
||||
ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"]
|
||||
ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"]
|
||||
AnyState = Union[ConnectionState, _WebhookState[BaseWebhook]]
|
||||
|
||||
AssetBytes = Union[utils._BytesLike, "AssetMixin"]
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
|
||||
MISSING = utils.MISSING
|
||||
|
||||
|
||||
class AssetMixin:
|
||||
url: str
|
||||
_state: Optional[AnyState]
|
||||
|
||||
__slots__: Tuple[str, ...] = ("_state",)
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if self._state is None:
|
||||
raise DiscordException("Invalid state (no ConnectionState provided)")
|
||||
|
||||
return await self._state.http.get_from_cdn(self.url)
|
||||
|
||||
async def save(
|
||||
self, fp: Union[str, bytes, os.PathLike, io.BufferedIOBase], *, seek_begin: bool = True
|
||||
) -> int:
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
|
||||
The file-like object to save this asset to or the filename
|
||||
to use. If a filename is passed then a file is created with that
|
||||
filename and used instead.
|
||||
seek_begin: :class:`bool`
|
||||
Whether to seek to the beginning of the file after saving is
|
||||
successfully done.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
"""
|
||||
data = await self.read()
|
||||
if isinstance(fp, io.BufferedIOBase):
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
else:
|
||||
with open(fp, "wb") as f:
|
||||
return f.write(data)
|
||||
|
||||
async def to_file(
|
||||
self,
|
||||
*,
|
||||
spoiler: bool = False,
|
||||
filename: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> File:
|
||||
"""|coro|
|
||||
|
||||
Converts the asset into a :class:`File` suitable for sending via
|
||||
:meth:`abc.Messageable.send`.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Raises :exc:`TypeError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
spoiler: :class:`bool`
|
||||
Whether the file is a spoiler.
|
||||
filename: Optional[:class:`str`]
|
||||
The filename to display when uploading to Discord. If this is not given, it defaults to
|
||||
the name of the asset's URL.
|
||||
description: Optional[:class:`str`]
|
||||
The file's description.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
The asset does not have an associated state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
TypeError
|
||||
The asset is a unicode emoji or a sticker with lottie type.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`File`
|
||||
The asset as a file suitable for sending.
|
||||
"""
|
||||
data = await self.read()
|
||||
|
||||
if not filename:
|
||||
filename = yarl.URL(self.url).name
|
||||
# if the filename doesn't have an extension (e.g. widget member avatars),
|
||||
# try to infer it from the data
|
||||
if not os.path.splitext(filename)[1]:
|
||||
ext = utils._get_extension_for_data(data)
|
||||
if ext:
|
||||
filename += ext
|
||||
|
||||
return File(io.BytesIO(data), filename=filename, spoiler=spoiler, description=description)
|
||||
|
||||
|
||||
class Asset(AssetMixin):
|
||||
"""Represents a CDN asset on Discord.
|
||||
|
||||
.. collapse:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the URL of the CDN asset.
|
||||
|
||||
.. describe:: len(x)
|
||||
|
||||
Returns the length of the CDN asset's URL.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the asset is equal to another asset.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the asset is not equal to another asset.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the asset.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"_url",
|
||||
"_animated",
|
||||
"_key",
|
||||
)
|
||||
|
||||
BASE = "https://cdn.discordapp.com"
|
||||
|
||||
# only used in special cases where Discord doesn't provide an asset on the CDN url
|
||||
BASE_MEDIA = "https://media.discordapp.net"
|
||||
|
||||
def __init__(self, state: AnyState, *, url: str, key: str, animated: bool = False) -> None:
|
||||
self._state: AnyState = state
|
||||
self._url: str = url
|
||||
self._animated: bool = animated
|
||||
self._key: str = key
|
||||
|
||||
@classmethod
|
||||
def _from_default_avatar(cls, state: AnyState, index: int) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/embed/avatars/{index}.png",
|
||||
key=str(index),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_avatar(cls, state: AnyState, user_id: int, avatar: str) -> Self:
|
||||
animated = avatar.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024",
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_avatar(
|
||||
cls, state: AnyState, guild_id: int, member_id: int, avatar: str
|
||||
) -> Self:
|
||||
animated = avatar.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024",
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_banner(
|
||||
cls, state: AnyState, guild_id: int, member_id: int, banner: str
|
||||
) -> Self:
|
||||
animated = banner.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024",
|
||||
key=banner,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state: AnyState, object_id: int, icon_hash: str, path: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024",
|
||||
key=icon_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_cover_image(cls, state: AnyState, object_id: int, cover_image_hash: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024",
|
||||
key=cover_image_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_image(cls, state: AnyState, guild_id: int, image: str, path: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024",
|
||||
key=image,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_icon(cls, state: AnyState, guild_id: int, icon_hash: str) -> Self:
|
||||
animated = icon_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024",
|
||||
key=icon_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_sticker_banner(cls, state: AnyState, banner: int) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png",
|
||||
key=str(banner),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_banner(cls, state: AnyState, id: int, banner_hash: str) -> Self:
|
||||
animated = banner_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/banners/{id}/{banner_hash}.{format}?size=1024",
|
||||
key=banner_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_role_icon(cls, state: AnyState, role_id: int, icon_hash: str) -> Self:
|
||||
animated = icon_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/role-icons/{role_id}/{icon_hash}.{format}?size=1024",
|
||||
key=icon_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_scheduled_event_image(
|
||||
cls, state: AnyState, event_id: int, image_hash: str
|
||||
) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guild-events/{event_id}/{image_hash}.png?size=2048",
|
||||
key=image_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_avatar_decoration(cls, state: AnyState, avatar_decoration_asset: str) -> Self:
|
||||
animated = avatar_decoration_asset.startswith("a_")
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/avatar-decoration-presets/{avatar_decoration_asset}.png?size=1024",
|
||||
key=avatar_decoration_asset,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_nameplate(cls, state: AnyState, nameplate_asset: str, animated: bool = True) -> Self:
|
||||
suffix = "asset.webm" if animated else "static.png"
|
||||
return cls(
|
||||
state,
|
||||
# nameplate_asset already includes an ending /
|
||||
url=f"{cls.BASE}/assets/collectibles/{nameplate_asset}{suffix}",
|
||||
key=nameplate_asset,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_tag_badge(cls, state: AnyState, primary_guild_id: int, badge_hash: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guild-tag-badges/{primary_guild_id}/{badge_hash}.png?size=16",
|
||||
key=badge_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._url
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._url)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
shorten = self._url.replace(self.BASE, "")
|
||||
return f"<Asset url={shorten!r}>"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._url)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the underlying URL of the asset."""
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
""":class:`str`: Returns the identifying key of the asset."""
|
||||
return self._key
|
||||
|
||||
def is_animated(self) -> bool:
|
||||
"""Whether the asset is animated.
|
||||
|
||||
:return type: :class:`bool`
|
||||
"""
|
||||
return self._animated
|
||||
|
||||
def replace(
|
||||
self,
|
||||
*,
|
||||
size: int = MISSING,
|
||||
format: ValidAssetFormatTypes = MISSING,
|
||||
static_format: ValidStaticFormatTypes = MISSING,
|
||||
) -> Asset:
|
||||
"""Returns a new asset with the passed components replaced.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Raises :exc:`ValueError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
format: :class:`str`
|
||||
The new format to change it to. Must be either
|
||||
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
|
||||
static_format: :class:`str`
|
||||
The new format to change it to if the asset isn't animated.
|
||||
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
An invalid size or format was passed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
|
||||
if format is not MISSING:
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise ValueError(f"format must be one of {VALID_ASSET_FORMATS}")
|
||||
else:
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
url = url.with_path(f"{path}.{format}")
|
||||
|
||||
if static_format is not MISSING and not self._animated:
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
url = url.with_path(f"{path}.{static_format}")
|
||||
|
||||
if size is not MISSING:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise ValueError("size must be a power of 2 between 16 and 4096")
|
||||
url = url.with_query(size=size)
|
||||
else:
|
||||
url = url.with_query(url.raw_query_string)
|
||||
|
||||
url_str = str(url)
|
||||
return Asset(state=self._state, url=url_str, key=self._key, animated=self._animated)
|
||||
|
||||
def with_size(self, size: int, /) -> Asset:
|
||||
"""Returns a new asset with the specified size.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Raises :exc:`ValueError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The asset had an invalid size.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
if not utils.valid_icon_size(size):
|
||||
raise ValueError("size must be a power of 2 between 16 and 4096")
|
||||
|
||||
url = str(yarl.URL(self._url).with_query(size=size))
|
||||
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_format(self, format: ValidAssetFormatTypes, /) -> Asset:
|
||||
"""Returns a new asset with the specified format.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Raises :exc:`ValueError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
format: :class:`str`
|
||||
The new format of the asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise ValueError(f"format must be one of {VALID_ASSET_FORMATS}")
|
||||
else:
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
url_str = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string))
|
||||
return Asset(state=self._state, url=url_str, key=self._key, animated=self._animated)
|
||||
|
||||
def with_static_format(self, format: ValidStaticFormatTypes, /) -> Asset:
|
||||
"""Returns a new asset with the specified static format.
|
||||
|
||||
This only changes the format if the underlying asset is
|
||||
not animated. Otherwise, the asset is not changed.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Raises :exc:`ValueError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
format: :class:`str`
|
||||
The new static format of the asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
if self._animated:
|
||||
return self
|
||||
return self.with_format(format)
|
||||
Reference in New Issue
Block a user