Adding all files

This commit is contained in:
2026-02-03 20:32:43 +02:00
parent 2588d10ba0
commit 77b70b600f
1457 changed files with 184865 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# SPDX-License-Identifier: MIT
"""Discord API Wrapper
~~~~~~~~~~~~~~~~~~~
A basic wrapper for the Discord API.
:copyright: (c) 2015-2021 Rapptz, 2021-present Disnake Development
:license: MIT, see LICENSE for more details.
"""
__title__ = "disnake"
__author__ = "Rapptz, EQUENOS"
__license__ = "MIT"
__copyright__ = "Copyright 2015-present Rapptz, 2021-present EQUENOS"
__version__ = "2.11.0"
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
import logging
from typing import Literal, NamedTuple
from . import abc as abc, opus as opus, ui as ui, utils as utils # explicitly re-export modules
from .activity import *
from .app_commands import *
from .appinfo import *
from .application_role_connection import *
from .asset import *
from .audit_logs import *
from .automod import *
from .bans import *
from .channel import *
from .client import *
from .colour import *
from .components import *
from .custom_warnings import *
from .embeds import *
from .emoji import *
from .entitlement import *
from .enums import *
from .errors import *
from .file import *
from .flags import *
from .guild import *
from .guild_preview import *
from .guild_scheduled_event import *
from .i18n import *
from .integrations import *
from .interactions import *
from .invite import *
from .member import *
from .mentions import *
from .message import *
from .object import *
from .onboarding import *
from .partial_emoji import *
from .permissions import *
from .player import *
from .poll import *
from .raw_models import *
from .reaction import *
from .role import *
from .shard import *
from .sku import *
from .soundboard import *
from .stage_instance import *
from .sticker import *
from .subscription import *
from .team import *
from .template import *
from .threads import *
from .user import *
from .voice_client import *
from .voice_region import *
from .webhook import *
from .welcome_screen import *
from .widget import *
class VersionInfo(NamedTuple):
major: int
minor: int
micro: int
releaselevel: Literal["alpha", "beta", "candidate", "final"]
serial: int
# fmt: off
version_info: VersionInfo = VersionInfo(major=2, minor=11, micro=0, releaselevel="final", serial=0)
# fmt: on
logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@@ -0,0 +1,419 @@
# SPDX-License-Identifier: MIT
import argparse
import importlib.metadata
import platform
import sys
from pathlib import Path
from typing import List, Tuple, Union
import aiohttp
import disnake
def show_version() -> None:
entries: List[str] = []
sys_ver = sys.version_info
entries.append(
f"- Python v{sys_ver.major}.{sys_ver.minor}.{sys_ver.micro}-{sys_ver.releaselevel}"
)
disnake_ver = disnake.version_info
entries.append(
f"- disnake v{disnake_ver.major}.{disnake_ver.minor}.{disnake_ver.micro}-{disnake_ver.releaselevel}"
)
try:
version = importlib.metadata.version("disnake")
except importlib.metadata.PackageNotFoundError:
pass
else:
entries.append(f" - disnake importlib.metadata: v{version}")
entries.append(f"- aiohttp v{aiohttp.__version__}")
uname = platform.uname()
entries.append(f"- system info: {uname.system} {uname.release} {uname.version} {uname.machine}")
print("\n".join(entries))
def core(parser: argparse.ArgumentParser, args) -> None:
# this method runs when no subcommands are provided
# as such, we can assume that we want to print help
if args.version:
show_version()
else:
parser.print_help()
_interaction_bot_init = """super().__init__(**kwargs)"""
_commands_bot_init = (
'super().__init__(command_prefix=commands.when_mentioned_or("{prefix}"), **kwargs)'
)
_bot_template = """#!/usr/bin/env python3
from disnake.ext import commands
import disnake
import config
class Bot(commands.{base}):
def __init__(self, **kwargs):
{init}
for cog in config.cogs:
try:
self.load_extension(cog)
except Exception as exc:
print(f"Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}")
async def on_ready(self):
print(f"Logged on as {{self.user}} (ID: {{self.user.id}})")
bot = Bot()
# write general commands here
if __name__ == "__main__":
bot.run(config.token)
"""
_gitignore_template = """# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Our configuration files
config.py
"""
_cog_template = '''from disnake.ext import commands
import disnake
class {name}(commands.Cog{attrs}):
"""The description for {name} goes here."""
def __init__(self, bot):
self.bot = bot
{extra}
def setup(bot):
bot.add_cog({name}(bot))
'''
# everything that is a _cog_special_method goes here.
_cog_extras = """
async def cog_load(self):
# (async) loading logic goes here
pass
def cog_unload(self):
# clean up logic goes here
pass
### Prefix Commands ###
async def cog_check(self, ctx):
# checks that apply to every prefix command in here
return True
async def bot_check(self, ctx):
# checks that apply to every prefix command to the bot
return True
async def bot_check_once(self, ctx):
# check that apply to every prefix command but is guaranteed to be called only once
return True
async def cog_command_error(self, ctx, error):
# error handling to every prefix command in here
pass
async def cog_before_invoke(self, ctx):
# called before a prefix command is called here
pass
async def cog_after_invoke(self, ctx):
# called after a prefix command is called here
pass
### Slash Commands ###
# These are similar to the ones in the previous section, but for slash commands
async def cog_slash_command_check(self, inter):
return True
async def bot_slash_command_check(self, inter):
return True
async def bot_slash_command_check_once(self, inter):
return True
async def cog_slash_command_error(self, inter, error):
...
async def cog_before_slash_command_invoke(self, inter):
...
async def cog_after_slash_command_invoke(self, inter):
...
### Message (Context Menu) Commands ###
async def cog_message_command_check(self, inter):
return True
async def bot_message_command_check(self, inter):
return True
async def bot_message_command_check_once(self, inter):
return True
async def cog_message_command_error(self, inter, error):
...
async def cog_before_message_command_invoke(self, inter):
...
async def cog_after_message_command_invoke(self, inter):
...
### User (Context Menu) Commands ###
async def cog_user_command_check(self, inter):
return True
async def bot_user_command_check(self, inter):
return True
async def bot_user_command_check_once(self, inter):
return True
async def cog_user_command_error(self, inter, error):
...
async def cog_before_user_command_invoke(self, inter):
...
async def cog_after_user_command_invoke(self, inter):
...
"""
# certain file names and directory names are forbidden
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
# although some of this doesn't apply to Linux, we might as well be consistent
_ascii_table = dict.fromkeys('<>:"|?*', "-")
# NUL (0) and 1-31 are disallowed
_byte_table = dict.fromkeys(map(chr, range(32)))
_base_table = {**_ascii_table, **_byte_table}
_translation_table = str.maketrans(_base_table)
def to_path(parser, name: Union[str, Path], *, replace_spaces: bool = False) -> Path:
if isinstance(name, Path):
return name
if sys.platform == "win32":
forbidden = (
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
)
if len(name) <= 4 and name.upper() in forbidden:
parser.error("invalid directory name given, use a different one")
name = name.translate(_translation_table)
if replace_spaces:
name = name.replace(" ", "-")
return Path(name)
def newbot(parser, args) -> None:
new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
# as a note exist_ok for Path is a 3.5+ only feature
# since we already checked above that we're >3.5
try:
new_directory.mkdir(exist_ok=True, parents=True)
except OSError as exc:
parser.error(f"could not create our bot directory ({exc})")
cogs = new_directory / "cogs"
try:
cogs.mkdir(exist_ok=True)
init = cogs / "__init__.py"
init.touch()
except OSError as exc:
print(f"warning: could not create cogs directory ({exc})")
try:
with open(str(new_directory / "config.py"), "w", encoding="utf-8") as fp:
fp.write('token = "place your token here"\ncogs = []\n')
except OSError as exc:
parser.error(f"could not create config file ({exc})")
try:
with open(str(new_directory / "bot.py"), "w", encoding="utf-8") as fp:
if args.interaction_client:
init = _interaction_bot_init
base = "AutoShardedInteractionBot" if args.sharded else "InteractionBot"
else:
init = _commands_bot_init.format(prefix=args.prefix)
base = "AutoShardedBot" if args.sharded else "Bot"
fp.write(_bot_template.format(base=base, init=init))
except OSError as exc:
parser.error(f"could not create bot file ({exc})")
if not args.no_git:
try:
with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp:
fp.write(_gitignore_template)
except OSError as exc:
print(f"warning: could not create .gitignore file ({exc})")
print("successfully made bot at", new_directory)
def newcog(parser, args) -> None:
cog_dir = to_path(parser, args.directory)
try:
cog_dir.mkdir(exist_ok=True)
except OSError as exc:
print(f"warning: could not create cogs directory ({exc})")
directory = cog_dir / to_path(parser, args.name)
directory = directory.with_suffix(".py")
try:
with open(str(directory), "w", encoding="utf-8") as fp:
attrs = ""
extra = _cog_extras if args.full else ""
if args.class_name:
name = args.class_name
else:
name = str(directory.stem)
if "-" in name or "_" in name:
translation = str.maketrans("-_", " ")
name = name.translate(translation).title().replace(" ", "")
else:
name = name.title()
if args.display_name:
attrs += f', name="{args.display_name}"'
if args.hide_commands:
attrs += ", command_attrs=dict(hidden=True)"
fp.write(_cog_template.format(name=name, extra=extra, attrs=attrs))
except OSError as exc:
parser.error(f"could not create cog file ({exc})")
else:
print("successfully made cog at", directory)
def add_newbot_args(subparser) -> None:
parser = subparser.add_parser("newbot", help="creates a command bot project quickly")
parser.set_defaults(func=newbot)
parser.add_argument("name", help="the bot project name")
parser.add_argument(
"directory", help="the directory to place it in (default: .)", nargs="?", default=Path.cwd()
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--prefix", help="the bot prefix (default: $)", default="$", metavar="<prefix>"
)
group.add_argument(
"--app-commands-only",
help="whether to only process application commands",
action="store_true",
dest="interaction_client",
)
parser.add_argument(
"--sharded", help="whether to use an automatically sharded bot", action="store_true"
)
parser.add_argument(
"--no-git", help="do not create a .gitignore file", action="store_true", dest="no_git"
)
def add_newcog_args(subparser) -> None:
parser = subparser.add_parser("newcog", help="creates a new cog template quickly")
parser.set_defaults(func=newcog)
parser.add_argument("name", help="the cog name")
parser.add_argument(
"directory",
help="the directory to place it in (default: cogs)",
nargs="?",
default=Path("cogs"),
)
parser.add_argument(
"--class-name", help="the class name of the cog (default: <name>)", dest="class_name"
)
parser.add_argument("--display-name", help="the cog name (default: <name>)")
parser.add_argument(
"--hide-commands", help="whether to hide all commands in the cog", action="store_true"
)
parser.add_argument("--full", help="add all special methods as well", action="store_true")
def parse_args() -> Tuple[argparse.ArgumentParser, argparse.Namespace]:
parser = argparse.ArgumentParser(prog="disnake", description="Tools for helping with disnake")
parser.add_argument("-v", "--version", action="store_true", help="shows the library version")
parser.set_defaults(func=core)
subparser = parser.add_subparsers(dest="subcommand", title="subcommands")
add_newbot_args(subparser)
add_newcog_args(subparser)
return parser, parser.parse_args()
def main() -> None:
parser, args = parse_args()
args.func(parser, args)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,974 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload
from .asset import Asset
from .colour import Colour
from .enums import ActivityType, StatusDisplayType, try_enum
from .partial_emoji import PartialEmoji
__all__ = (
"BaseActivity",
"Activity",
"Streaming",
"Game",
"Spotify",
"CustomActivity",
)
"""If curious, this is the current schema for an activity.
It's fairly long so I will document it here:
All keys are optional.
state: str (max: 128),
details: str (max: 128)
timestamps: dict
start: int (min: 1)
end: int (min: 1)
assets: dict
large_image: str (max: 32)
large_text: str (max: 128)
small_image: str (max: 32)
small_text: str (max: 128)
party: dict
id: str (max: 128),
size: List[int] (max-length: 2)
elem: int (min: 1)
secrets: dict
match: str (max: 128)
join: str (max: 128)
spectate: str (max: 128)
instance: bool
application_id: str
name: str (max: 128)
url: str
type: int
sync_id: str
session_id: str
flags: int
buttons: list[str (max: 32)]
There are also activity flags which are mostly uninteresting for the library atm.
t.ActivityFlags = {
INSTANCE: 1,
JOIN: 2,
SPECTATE: 4,
JOIN_REQUEST: 8,
SYNC: 16,
PLAY: 32
}
"""
if TYPE_CHECKING:
from .state import ConnectionState
from .types.activity import (
Activity as ActivityPayload,
ActivityAssets,
ActivityEmoji as ActivityEmojiPayload,
ActivityParty,
ActivityTimestamps,
)
from .types.emoji import PartialEmoji as PartialEmojiPayload
from .types.widget import WidgetActivity as WidgetActivityPayload
class _BaseActivity:
__slots__ = ("_created_at", "_timestamps", "assets")
def __init__(
self,
*,
created_at: Optional[float] = None,
timestamps: Optional[ActivityTimestamps] = None,
assets: Optional[ActivityAssets] = None,
**kwargs: Any, # discarded
) -> None:
self._created_at: Optional[float] = created_at
self._timestamps: ActivityTimestamps = timestamps or {}
self.assets: ActivityAssets = assets or {}
@property
def created_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC.
.. versionadded:: 1.3
"""
if self._created_at is not None:
return datetime.datetime.fromtimestamp(
self._created_at / 1000, tz=datetime.timezone.utc
)
@property
def start(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.
.. versionchanged:: 2.6
This attribute can now be ``None``.
"""
try:
timestamp = self._timestamps["start"] / 1000
except KeyError:
return None
else:
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
@property
def end(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.
.. versionchanged:: 2.6
This attribute can now be ``None``.
"""
try:
timestamp = self._timestamps["end"] / 1000
except KeyError:
return None
else:
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
def to_dict(self) -> ActivityPayload:
raise NotImplementedError
def _create_image_url(self, asset: str) -> Optional[str]:
# `asset` can be a simple ID (see `Activity._create_image_url`),
# or a string of the format `<prefix>:<id>`
prefix, _, asset_id = asset.partition(":")
if asset_id and (url_fmt := _ACTIVITY_URLS.get(prefix)):
return url_fmt.format(asset_id)
return None
@property
def large_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
Additionally, supports dynamic asset urls using the ``mp:`` prefix now.
"""
try:
large_image = self.assets["large_image"]
except KeyError:
return None
else:
return self._create_image_url(large_image)
@property
def small_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
Additionally, supports dynamic asset urls using the ``mp:`` prefix now.
"""
try:
small_image = self.assets["small_image"]
except KeyError:
return None
else:
return self._create_image_url(small_image)
@property
def large_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
"""
return self.assets.get("large_text")
@property
def small_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
"""
return self.assets.get("small_text")
@property
def large_image_link(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the large image asset URL of this activity, if applicable.
.. versionadded:: 2.11
"""
return self.assets.get("large_url")
@property
def small_image_link(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the small image asset URL of this activity, if applicable.
.. versionadded:: 2.11
"""
return self.assets.get("small_url")
# tag type for user-settable activities
class BaseActivity(_BaseActivity):
"""The base activity that all user-settable activities inherit from.
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
The following types currently count as user-settable:
- :class:`Activity`
- :class:`Game`
- :class:`Streaming`
- :class:`CustomActivity`
Note that although these types are considered user-settable by the library,
Discord typically ignores certain combinations of activity depending on
what is currently set. This behaviour may change in the future so there are
no guarantees on whether Discord will actually let you set these types.
.. versionadded:: 1.3
"""
__slots__ = ()
# There are additional urls for twitch/youtube/spotify, however
# it appears that Discord does not want to document those:
# https://github.com/discord/discord-api-docs/pull/4617
# They are partially supported by different properties, e.g. `Spotify.album_cover_url`.
_ACTIVITY_URLS = {
"mp": "https://media.discordapp.net/{}",
}
class Activity(BaseActivity):
"""Represents an activity in Discord.
This could be an activity such as streaming, playing, listening
or watching.
For memory optimisation purposes, some activities are offered in slimmed
down versions:
- :class:`Game`
- :class:`Streaming`
Parameters
----------
name: Optional[:class:`str`]
The name of the activity.
url: Optional[:class:`str`]
A stream URL that the activity could be doing.
type: :class:`ActivityType`
The type of activity currently being done.
Attributes
----------
application_id: Optional[:class:`int`]
The application ID of the game.
name: Optional[:class:`str`]
The name of the activity.
url: Optional[:class:`str`]
A stream URL that the activity could be doing.
type: :class:`ActivityType`
The type of activity currently being done.
state: Optional[:class:`str`]
The user's current state. For example, "In Game".
details: Optional[:class:`str`]
The detail of the user's current activity.
assets: :class:`dict`
A dictionary representing the images and their hover text of an activity.
It contains the following optional keys:
- ``large_image``: A string representing the ID for the large image asset.
- ``large_text``: A string representing the text when hovering over the large image asset.
- ``large_url``: A string representing an URL that is opened when clicking on the large image.
- ``small_image``: A string representing the ID for the small image asset.
- ``small_text``: A string representing the text when hovering over the small image asset.
- ``small_url``: A string representing a URL that is opened when clicking on the small image.
party: :class:`dict`
A dictionary representing the activity party. It contains the following optional keys:
- ``id``: A string representing the party ID.
- ``size``: A list of two integers denoting (current_size, maximum_size).
buttons: List[str]
A list of strings representing the labels of custom buttons shown in a rich presence.
.. versionadded:: 2.0
.. versionchanged:: 2.6
Changed type to ``List[str]`` to match API types.
emoji: Optional[:class:`PartialEmoji`]
The emoji that belongs to this activity.
details_url: Optional[:class:`str`]
An URL that is linked when clicking on the details text of an activity.
.. versionadded:: 2.11
state_url: Optional[:class:`str`]
An URL that is linked when clicking on the state text of an activity.
.. versionadded:: 2.11
status_display_type: Optional[:class:`StatusDisplayType`]
Controls which field is displayed in the user's status activity text in the member list.
.. versionadded:: 2.11
"""
__slots__ = (
"state",
"details",
"party",
"flags",
"type",
"name",
"url",
"application_id",
"emoji",
"buttons",
"id",
"platform",
"sync_id",
"session_id",
"details_url",
"state_url",
"status_display_type",
)
def __init__(
self,
*,
name: Optional[str] = None,
url: Optional[str] = None,
type: Optional[Union[ActivityType, int]] = None,
state: Optional[str] = None,
state_url: Optional[str] = None,
details: Optional[str] = None,
details_url: Optional[str] = None,
party: Optional[ActivityParty] = None,
application_id: Optional[Union[str, int]] = None,
flags: Optional[int] = None,
buttons: Optional[List[str]] = None,
emoji: Optional[Union[PartialEmojiPayload, ActivityEmojiPayload]] = None,
id: Optional[str] = None,
platform: Optional[str] = None,
sync_id: Optional[str] = None,
session_id: Optional[str] = None,
status_display_type: Optional[Union[StatusDisplayType, int]] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.state: Optional[str] = state
self.state_url: Optional[str] = state_url
self.details: Optional[str] = details
self.details_url: Optional[str] = details_url
self.party: ActivityParty = party or {}
self.application_id: Optional[int] = (
int(application_id) if application_id is not None else None
)
self.name: Optional[str] = name
self.url: Optional[str] = url
self.flags: int = flags or 0
self.buttons: List[str] = buttons or []
# undocumented fields:
self.id: Optional[str] = id
self.platform: Optional[str] = platform
self.sync_id: Optional[str] = sync_id
self.session_id: Optional[str] = session_id
activity_type = type if type is not None else 0
self.type: ActivityType = (
activity_type
if isinstance(activity_type, ActivityType)
else try_enum(ActivityType, activity_type)
)
self.status_display_type: Optional[StatusDisplayType] = (
try_enum(StatusDisplayType, status_display_type)
if isinstance(status_display_type, int)
else status_display_type
)
self.emoji: Optional[PartialEmoji] = (
PartialEmoji.from_dict(emoji) if emoji is not None else None
)
def __repr__(self) -> str:
attrs = (
("type", self.type),
("name", self.name),
("url", self.url),
("details", self.details),
("application_id", self.application_id),
("session_id", self.session_id),
("emoji", self.emoji),
)
inner = " ".join(f"{k!s}={v!r}" for k, v in attrs)
return f"<Activity {inner}>"
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {}
for attr in self.__slots__:
value = getattr(self, attr, None)
if value is None:
continue
if isinstance(value, dict) and len(value) == 0:
continue
ret[attr] = value
# fix type field
ret["type"] = int(self.type)
if self.status_display_type:
ret["status_display_type"] = int(self.status_display_type)
if self.emoji:
ret["emoji"] = self.emoji.to_dict()
# defined in base class slots
if self._timestamps:
ret["timestamps"] = self._timestamps
return ret
def _create_image_url(self, asset: str) -> Optional[str]:
# if parent method already returns valid url, use that
if url := super()._create_image_url(asset):
return url
# if it's not a `<prefix>:<id>` asset and we have an application ID, create url
if ":" not in asset and self.application_id:
return f"{Asset.BASE}/app-assets/{self.application_id}/{asset}.png"
# else, it's an unknown asset url
return None
class Game(BaseActivity):
"""A slimmed down version of :class:`Activity` that represents a Discord game.
This is typically displayed via **Playing** on the official Discord client.
.. collapse:: operations
.. describe:: x == y
Checks if two games are equal.
.. describe:: x != y
Checks if two games are not equal.
.. describe:: hash(x)
Returns the game's hash.
.. describe:: str(x)
Returns the game's name.
Parameters
----------
name: :class:`str`
The game's name.
Attributes
----------
name: :class:`str`
The game's name.
assets: :class:`dict`
A dictionary with the same structure as :attr:`Activity.assets`.
"""
__slots__ = ("name", "platform")
def __init__(
self,
name: str,
*,
platform: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.name: str = name
# undocumented
self.platform: Optional[str] = platform
@property
def type(self) -> Literal[ActivityType.playing]:
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.playing`.
"""
return ActivityType.playing
def __str__(self) -> str:
return str(self.name)
def __repr__(self) -> str:
return f"<Game name={self.name!r}>"
def to_dict(self) -> ActivityPayload:
return {
"type": ActivityType.playing.value,
"name": str(self.name),
"timestamps": self._timestamps,
"assets": self.assets,
}
def __eq__(self, other: Any) -> bool:
return isinstance(other, Game) and other.name == self.name
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return hash(self.name)
class Streaming(BaseActivity):
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
This is typically displayed via **Streaming** on the official Discord client.
.. collapse:: operations
.. describe:: x == y
Checks if two streams are equal.
.. describe:: x != y
Checks if two streams are not equal.
.. describe:: hash(x)
Returns the stream's hash.
.. describe:: str(x)
Returns the stream's name.
Attributes
----------
platform: Optional[:class:`str`]
Where the user is streaming from (ie. YouTube, Twitch).
.. versionadded:: 1.3
name: Optional[:class:`str`]
The stream's name.
details: Optional[:class:`str`]
An alias for :attr:`name`
game: Optional[:class:`str`]
The game being streamed.
.. versionadded:: 1.3
url: :class:`str`
The stream's URL.
assets: :class:`dict`
A dictionary with the same structure as :attr:`Activity.assets`.
"""
__slots__ = ("platform", "name", "game", "url", "details")
def __init__(
self,
*,
name: Optional[str],
url: str,
details: Optional[str] = None,
state: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.platform: Optional[str] = name
self.name: Optional[str] = details or name
self.details: Optional[str] = self.name # compatibility
self.url: str = url
self.game: Optional[str] = state
@property
def type(self) -> Literal[ActivityType.streaming]:
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.streaming`.
"""
return ActivityType.streaming
def __str__(self) -> str:
return str(self.name)
def __repr__(self) -> str:
return f"<Streaming name={self.name!r}>"
@property
def twitch_name(self) -> Optional[str]:
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
"""
try:
name = self.assets["large_image"]
except KeyError:
return None
else:
return name[7:] if name[:7] == "twitch:" else None
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {
"type": ActivityType.streaming.value,
"name": str(self.name),
"url": str(self.url),
"assets": self.assets,
}
if self.details:
ret["details"] = self.details
return ret
def __eq__(self, other: Any) -> bool:
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return hash(self.name)
class Spotify(_BaseActivity):
"""Represents a Spotify listening activity from Discord.
.. collapse:: operations
.. describe:: x == y
Checks if two activities are equal.
.. describe:: x != y
Checks if two activities are not equal.
.. describe:: hash(x)
Returns the activity's hash.
.. describe:: str(x)
Returns the string 'Spotify'.
"""
__slots__ = (
"_state",
"_details",
"_party",
"_sync_id",
"_session_id",
)
def __init__(
self,
*,
state: Optional[str] = None,
details: Optional[str] = None,
party: Optional[ActivityParty] = None,
sync_id: Optional[str] = None,
session_id: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self._state: str = state or ""
self._details: str = details or ""
self._party: ActivityParty = party or {}
self._sync_id: str = sync_id or ""
self._session_id: Optional[str] = session_id
@property
def type(self) -> Literal[ActivityType.listening]:
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.listening`.
"""
return ActivityType.listening
@property
def colour(self) -> Colour:
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :attr:`color`
"""
return Colour(0x1DB954)
@property
def color(self) -> Colour:
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :attr:`colour`
"""
return self.colour
def to_dict(self) -> Dict[str, Any]:
return {
"flags": 48, # SYNC | PLAY
"name": "Spotify",
"assets": self.assets,
"party": self._party,
"sync_id": self._sync_id,
"session_id": self._session_id,
"timestamps": self._timestamps,
"details": self._details,
"state": self._state,
}
@property
def name(self) -> str:
""":class:`str`: The activity's name. This will always return "Spotify"."""
return "Spotify"
def __eq__(self, other: Any) -> bool:
return (
isinstance(other, Spotify)
and other._session_id == self._session_id
and other._sync_id == self._sync_id
and other.start == self.start
)
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return hash(self._session_id)
def __str__(self) -> str:
return "Spotify"
def __repr__(self) -> str:
return f"<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>"
@property
def title(self) -> str:
""":class:`str`: The title of the song being played."""
return self._details
@property
def artists(self) -> List[str]:
"""List[:class:`str`]: The artists of the song being played."""
return self._state.split("; ")
@property
def artist(self) -> str:
""":class:`str`: The artist of the song being played.
This does not attempt to split the artist information into
multiple artists. Useful if there's only a single artist.
"""
return self._state
@property
def album(self) -> str:
""":class:`str`: The album that the song being played belongs to."""
return self.assets.get("large_text", "")
@property
def album_cover_url(self) -> str:
""":class:`str`: The album cover image URL from Spotify's CDN."""
large_image = self.assets.get("large_image", "")
if large_image[:8] != "spotify:":
return ""
album_image_id = large_image[8:]
return f"https://i.scdn.co/image/{album_image_id}"
@property
def track_id(self) -> str:
""":class:`str`: The track ID used by Spotify to identify this song."""
return self._sync_id
@property
def track_url(self) -> str:
""":class:`str`: The track URL to listen on Spotify.
.. versionadded:: 2.0
"""
return f"https://open.spotify.com/track/{self.track_id}"
@property
def duration(self) -> Optional[datetime.timedelta]:
"""Optional[:class:`datetime.timedelta`]: The duration of the song being played, if applicable.
.. versionchanged:: 2.6
This attribute can now be ``None``.
"""
start, end = self.start, self.end
if start and end:
return end - start
return None
@property
def party_id(self) -> str:
""":class:`str`: The party ID of the listening party."""
return self._party.get("id", "")
class CustomActivity(BaseActivity):
"""Represents a Custom activity from Discord.
.. collapse:: operations
.. describe:: x == y
Checks if two activities are equal.
.. describe:: x != y
Checks if two activities are not equal.
.. describe:: hash(x)
Returns the activity's hash.
.. describe:: str(x)
Returns the custom status text.
.. versionadded:: 1.3
Attributes
----------
name: Optional[:class:`str`]
The custom activity's name.
emoji: Optional[:class:`PartialEmoji`]
The emoji to pass to the activity, if any.
This currently cannot be set by bots.
"""
__slots__ = ("name", "emoji", "state")
def __init__(
self,
name: Optional[str],
*,
emoji: Optional[Union[ActivityEmojiPayload, str, PartialEmoji]] = None,
state: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.name: Optional[str] = name
# Fall back to `name`, since `state` is the relevant field for custom status (`name` is not shown)
self.state: Optional[str] = state or name
# The official client uses "Custom Status" as the name, the actual name is in `state`
if self.name == "Custom Status":
self.name = self.state
self.emoji: Optional[PartialEmoji]
if emoji is None:
self.emoji = emoji
elif isinstance(emoji, dict):
self.emoji = PartialEmoji.from_dict(emoji)
elif isinstance(emoji, str):
self.emoji = PartialEmoji(name=emoji)
elif isinstance(emoji, PartialEmoji):
self.emoji = emoji
else:
raise TypeError(
f"Expected str, PartialEmoji, or None, received {type(emoji)!r} instead."
)
@property
def type(self) -> Literal[ActivityType.custom]:
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.custom`.
"""
return ActivityType.custom
def to_dict(self) -> ActivityPayload:
o: ActivityPayload
if self.name == self.state:
o = {
"type": ActivityType.custom.value,
"state": self.name,
"name": "Custom Status",
}
else:
o = {
"type": ActivityType.custom.value,
"name": self.name or "",
}
if self.emoji:
o["emoji"] = self.emoji.to_dict() # type: ignore
return o
def __eq__(self, other: Any) -> bool:
return (
isinstance(other, CustomActivity)
and other.name == self.name
and other.emoji == self.emoji
)
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return hash((self.name, str(self.emoji)))
def __str__(self) -> str:
if self.emoji:
if self.name:
return f"{self.emoji} {self.name}"
return str(self.emoji)
else:
return str(self.name)
def __repr__(self) -> str:
return f"<CustomActivity name={self.name!r} emoji={self.emoji!r}>"
ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify]
@overload
def create_activity(
data: Union[ActivityPayload, WidgetActivityPayload], *, state: Optional[ConnectionState] = None
) -> ActivityTypes: ...
@overload
def create_activity(data: None, *, state: Optional[ConnectionState] = None) -> None: ...
def create_activity(
data: Optional[Union[ActivityPayload, WidgetActivityPayload]],
*,
state: Optional[ConnectionState] = None,
) -> Optional[ActivityTypes]:
if not data:
return None
activity: ActivityTypes
game_type = try_enum(ActivityType, data.get("type", -1))
if game_type is ActivityType.playing and not (
"application_id" in data or "session_id" in data or "state" in data
):
activity = Game(**data) # type: ignore # pyright bug(?)
elif game_type is ActivityType.custom and "name" in data:
activity = CustomActivity(**data) # type: ignore
elif game_type is ActivityType.streaming and "url" in data:
# url won't be None here
activity = Streaming(**data) # type: ignore
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
activity = Spotify(**data)
else:
activity = Activity(**data) # type: ignore
if isinstance(activity, (Activity, CustomActivity)) and activity.emoji and state:
activity.emoji._state = state
return activity

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Dict, List, Optional, cast
from . import utils
from .asset import Asset
from .enums import ApplicationEventWebhookStatus, try_enum
from .flags import ApplicationFlags
from .permissions import Permissions
if TYPE_CHECKING:
from .guild import Guild
from .state import ConnectionState
from .types.appinfo import (
AppInfo as AppInfoPayload,
ApplicationIntegrationType as ApplicationIntegrationTypeLiteral,
ApplicationIntegrationTypeConfiguration as ApplicationIntegrationTypeConfigurationPayload,
InstallParams as InstallParamsPayload,
PartialAppInfo as PartialAppInfoPayload,
Team as TeamPayload,
)
from .user import User
__all__ = (
"AppInfo",
"PartialAppInfo",
"InstallParams",
"InstallTypeConfiguration",
)
class InstallParams:
"""Represents the installation parameters for the application, provided by Discord.
.. versionadded:: 2.5
Attributes
----------
scopes: List[:class:`str`]
The scopes requested by the application.
permissions: :class:`Permissions`
The permissions requested for the bot role.
"""
__slots__ = (
"_app_id",
"_install_type",
"scopes",
"permissions",
)
def __init__(
self,
data: InstallParamsPayload,
parent: AppInfo,
*,
install_type: Optional[ApplicationIntegrationTypeLiteral] = None,
) -> None:
self._app_id = parent.id
self._install_type: Optional[ApplicationIntegrationTypeLiteral] = install_type
self.scopes = data["scopes"]
self.permissions = Permissions(int(data["permissions"]))
def __repr__(self) -> str:
return f"<InstallParams scopes={self.scopes!r} permissions={self.permissions!r}>"
def to_url(self) -> str:
"""Returns a string that can be used to install this application.
Returns
-------
:class:`str`
The invite url.
"""
return utils.oauth_url(
self._app_id,
scopes=self.scopes,
permissions=self.permissions,
integration_type=(
self._install_type if self._install_type is not None else utils.MISSING
),
)
class InstallTypeConfiguration:
"""Represents the configuration for a particular application installation type.
.. versionadded:: 2.10
Attributes
----------
install_params: Optional[:class:`InstallParams`]
The parameters for this installation type.
"""
__slots__ = ("install_params",)
def __init__(
self,
data: ApplicationIntegrationTypeConfigurationPayload,
*,
parent: AppInfo,
install_type: ApplicationIntegrationTypeLiteral,
) -> None:
self.install_params: Optional[InstallParams] = (
InstallParams(install_params, parent=parent, install_type=install_type)
if (install_params := data.get("oauth2_install_params"))
else None
)
class AppInfo:
"""Represents the application info for the bot provided by Discord.
Attributes
----------
id: :class:`int`
The application's ID.
name: :class:`str`
The application's name.
owner: :class:`User`
The application's owner.
team: Optional[:class:`Team`]
The application's team.
.. versionadded:: 1.3
description: :class:`str`
The application's description.
bot_public: :class:`bool`
Whether the bot can be invited by anyone or if it is locked
to the application owner.
bot_require_code_grant: :class:`bool`
Whether the bot requires the completion of the full oauth2 code
grant flow to join.
rpc_origins: Optional[List[:class:`str`]]
A list of RPC origin URLs, if RPC is enabled.
verify_key: :class:`str`
The hex encoded key for verification in interactions and the
GameSDK's :ddocs:`GetTicket <game-sdk/applications#getticket>`.
.. versionadded:: 1.3
guild_id: Optional[:class:`int`]
The ID of the guild associated with the application, if any.
.. versionadded:: 1.3
primary_sku_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the ID of the "Game SKU" that is created,
if it exists.
.. versionadded:: 1.3
slug: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the URL slug that links to the store page.
.. versionadded:: 1.3
terms_of_service_url: Optional[:class:`str`]
The application's terms of service URL, if set.
.. versionadded:: 2.0
privacy_policy_url: Optional[:class:`str`]
The application's privacy policy URL, if set.
.. versionadded:: 2.0
flags: Optional[:class:`ApplicationFlags`]
The application's public flags.
.. versionadded:: 2.3
tags: Optional[List[:class:`str`]]
The application's tags.
.. versionadded:: 2.5
install_params: Optional[:class:`InstallParams`]
The installation parameters for this application.
See also :attr:`guild_install_type_config`/:attr:`user_install_type_config`
for installation type-specific configuration.
.. versionadded:: 2.5
custom_install_url: Optional[:class:`str`]
The custom installation url for this application.
.. versionadded:: 2.5
role_connections_verification_url: Optional[:class:`str`]
The application's role connection verification entry point,
which when configured will render the app as a verification method
in the guild role verification configuration.
.. versionadded:: 2.8
approximate_guild_count: :class:`int`
The approximate number of guilds the application is installed to.
.. versionadded:: 2.10
approximate_user_install_count: :class:`int`
The approximate number of users that have installed the application
(for user-installable apps).
.. versionadded:: 2.10
approximate_user_authorization_count: :class:`int`
The approximate number of users that have authorized the app with OAuth2.
.. versionadded:: 2.11
redirect_uris: Optional[List[:class:`str`]]
The application's OAuth2 redirect URIs.
.. versionadded:: 2.11
interactions_endpoint_url: Optional[:class:`str`]
The application's interactions endpoint URL.
.. versionadded:: 2.11
event_webhooks_url: Optional[:class:`str`]
The application's event webhooks URL.
.. versionadded:: 2.11
event_webhooks_status: :class:`ApplicationEventWebhookStatus`
The application's event webhooks status.
.. versionadded:: 2.11
event_webhooks_types: Optional[List[:class:`str`]]
The application's event webhook types, if any.
.. versionadded:: 2.11
"""
__slots__ = (
"_state",
"description",
"id",
"name",
"rpc_origins",
"bot_public",
"bot_require_code_grant",
"owner",
"_icon",
"_summary",
"verify_key",
"team",
"guild_id",
"primary_sku_id",
"slug",
"_cover_image",
"terms_of_service_url",
"privacy_policy_url",
"flags",
"tags",
"install_params",
"redirect_uris",
"custom_install_url",
"interactions_endpoint_url",
"role_connections_verification_url",
"event_webhooks_url",
"event_webhooks_status",
"event_webhooks_types",
"approximate_guild_count",
"approximate_user_install_count",
"approximate_user_authorization_count",
"_install_types_config",
)
def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None:
from .team import Team
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.name: str = data["name"]
self.description: str = data["description"]
self._icon: Optional[str] = data["icon"]
self.rpc_origins: List[str] = data.get("rpc_origins") or []
self.bot_public: bool = data["bot_public"]
self.bot_require_code_grant: bool = data["bot_require_code_grant"]
self.owner: User = state.create_user(data["owner"])
team: Optional[TeamPayload] = data.get("team")
self.team: Optional[Team] = Team(state, team) if team else None
self._summary: str = data.get("summary", "")
self.verify_key: str = data["verify_key"]
self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id")
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, "primary_sku_id")
self.slug: Optional[str] = data.get("slug")
self._cover_image: Optional[str] = data.get("cover_image")
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
flags: Optional[int] = data.get("flags")
self.flags: Optional[ApplicationFlags] = (
ApplicationFlags._from_value(flags) if flags is not None else None
)
self.tags: Optional[List[str]] = data.get("tags")
self.install_params: Optional[InstallParams] = (
InstallParams(data["install_params"], parent=self) if "install_params" in data else None
)
self.custom_install_url: Optional[str] = data.get("custom_install_url")
self.redirect_uris: Optional[List[str]] = data.get("redirect_uris")
self.interactions_endpoint_url: Optional[str] = data.get("interactions_endpoint_url")
self.role_connections_verification_url: Optional[str] = data.get(
"role_connections_verification_url"
)
self.event_webhooks_url: Optional[str] = data.get("event_webhooks_url")
self.event_webhooks_status: ApplicationEventWebhookStatus = try_enum(
ApplicationEventWebhookStatus, data.get("event_webhooks_status", 1)
)
self.event_webhooks_types: Optional[List[str]] = data.get("event_webhooks_types")
self.approximate_guild_count: int = data.get("approximate_guild_count", 0)
self.approximate_user_install_count: int = data.get("approximate_user_install_count", 0)
self.approximate_user_authorization_count: int = data.get(
"approximate_user_authorization_count", 0
)
# this is a bit of a mess, but there's no better way to expose this data for now
self._install_types_config: Dict[
ApplicationIntegrationTypeLiteral, InstallTypeConfiguration
] = {}
for type_str, config in (data.get("integration_types_config") or {}).items():
install_type = cast("ApplicationIntegrationTypeLiteral", int(type_str))
self._install_types_config[install_type] = InstallTypeConfiguration(
config or {},
parent=self,
install_type=install_type,
)
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
f"description={self.description!r} public={self.bot_public} "
f"owner={self.owner!r}>"
)
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path="app")
@property
def cover_image(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the rich presence cover image asset, if any."""
if self._cover_image is None:
return None
return Asset._from_cover_image(self._state, self.id, self._cover_image)
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild associated with the application, if any.
.. versionadded:: 1.3
"""
return self._state._get_guild(self.guild_id)
@property
def summary(self) -> str:
""":class:`str`: If this application is a game sold on Discord,
this field will be the summary field for the store page of its primary SKU.
.. versionadded:: 1.3
.. deprecated:: 2.5
This field is deprecated by discord and is now always blank. Consider using :attr:`.description` instead.
"""
utils.warn_deprecated(
"summary is deprecated and will be removed in a future version. Consider using description instead.",
stacklevel=2,
)
return self._summary
@property
def guild_install_type_config(self) -> Optional[InstallTypeConfiguration]:
"""Optional[:class:`InstallTypeConfiguration`]: The guild installation parameters for
this application. If this application cannot be installed to guilds, returns ``None``.
.. versionadded:: 2.10
"""
return self._install_types_config.get(0)
@property
def user_install_type_config(self) -> Optional[InstallTypeConfiguration]:
"""Optional[:class:`InstallTypeConfiguration`]: The user installation parameters for
this application. If this application cannot be installed to users, returns ``None``.
.. versionadded:: 2.10
"""
return self._install_types_config.get(1)
class PartialAppInfo:
"""Represents a partial AppInfo given by :func:`~disnake.abc.GuildChannel.create_invite`.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The application's ID.
name: :class:`str`
The application's name.
description: :class:`str`
The application's description.
rpc_origins: Optional[List[:class:`str`]]
A list of RPC origin URLs, if RPC is enabled.
verify_key: :class:`str`
The hex encoded key for verification in interactions and the
GameSDK's :ddocs:`GetTicket <game-sdk/applications#getticket>`.
terms_of_service_url: Optional[:class:`str`]
The application's terms of service URL, if set.
privacy_policy_url: Optional[:class:`str`]
The application's privacy policy URL, if set.
"""
__slots__ = (
"_state",
"id",
"name",
"description",
"rpc_origins",
"_summary",
"verify_key",
"terms_of_service_url",
"privacy_policy_url",
"_icon",
)
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload) -> None:
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.name: str = data["name"]
self._icon: Optional[str] = data.get("icon")
self.description: str = data["description"]
self.rpc_origins: Optional[List[str]] = data.get("rpc_origins")
self._summary: str = data.get("summary", "")
self.verify_key: str = data["verify_key"]
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>"
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path="app")
@property
def summary(self) -> str:
""":class:`str`: If this application is a game sold on Discord,
this field will be the summary field for the store page of its primary SKU.
.. deprecated:: 2.5
This field is deprecated by discord and is now always blank. Consider using :attr:`.description` instead.
"""
utils.warn_deprecated(
"summary is deprecated and will be removed in a future version. Consider using description instead.",
stacklevel=2,
)
return self._summary

View File

@@ -0,0 +1,112 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING
from .enums import ApplicationRoleConnectionMetadataType, enum_if_int, try_enum
from .i18n import LocalizationValue, Localized
if TYPE_CHECKING:
from typing_extensions import Self
from .i18n import LocalizationProtocol, LocalizedRequired
from .types.application_role_connection import (
ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload,
)
__all__ = ("ApplicationRoleConnectionMetadata",)
class ApplicationRoleConnectionMetadata:
"""Represents the role connection metadata of an application.
See the :ddocs:`API documentation <resources/application-role-connection-metadata#application-role-connection-metadata-object>`
for further details and limits.
The list of metadata records associated with the current application/bot
can be retrieved/edited using :meth:`Client.fetch_role_connection_metadata`
and :meth:`Client.edit_role_connection_metadata`.
.. versionadded:: 2.8
Attributes
----------
type: :class:`ApplicationRoleConnectionMetadataType`
The type of the metadata value.
key: :class:`str`
The dictionary key for the metadata field.
name: :class:`str`
The name of the metadata field.
name_localizations: :class:`LocalizationValue`
The localizations for :attr:`name`.
description: :class:`str`
The description of the metadata field.
description_localizations: :class:`LocalizationValue`
The localizations for :attr:`description`.
"""
__slots__ = (
"type",
"key",
"name",
"name_localizations",
"description",
"description_localizations",
)
def __init__(
self,
*,
type: ApplicationRoleConnectionMetadataType,
key: str,
name: LocalizedRequired,
description: LocalizedRequired,
) -> None:
self.type: ApplicationRoleConnectionMetadataType = enum_if_int(
ApplicationRoleConnectionMetadataType, type
)
self.key: str = key
name_loc = Localized._cast(name, True)
self.name: str = name_loc.string
self.name_localizations: LocalizationValue = name_loc.localizations
desc_loc = Localized._cast(description, True)
self.description: str = desc_loc.string
self.description_localizations: LocalizationValue = desc_loc.localizations
def __repr__(self) -> str:
return (
f"<ApplicationRoleConnectionMetadata name={self.name!r} key={self.key!r} "
f"description={self.description!r} type={self.type!r}>"
)
@classmethod
def _from_data(cls, data: ApplicationRoleConnectionMetadataPayload) -> Self:
return cls(
type=try_enum(ApplicationRoleConnectionMetadataType, data["type"]),
key=data["key"],
name=Localized(data["name"], data=data.get("name_localizations")),
description=Localized(data["description"], data=data.get("description_localizations")),
)
def to_dict(self) -> ApplicationRoleConnectionMetadataPayload:
data: ApplicationRoleConnectionMetadataPayload = {
"type": self.type.value,
"key": self.key,
"name": self.name,
"description": self.description,
}
if (loc := self.name_localizations.data) is not None:
data["name_localizations"] = loc
if (loc := self.description_localizations.data) is not None:
data["description_localizations"] = loc
return data
def _localize(self, store: LocalizationProtocol) -> None:
self.name_localizations._link(store)
self.description_localizations._link(store)

View 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)

View File

@@ -0,0 +1,894 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
Generator,
List,
Mapping,
Optional,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from . import abc, enums, flags, utils
from .app_commands import ApplicationCommandPermissions
from .asset import Asset
from .automod import AutoModAction, AutoModTriggerMetadata, _automod_action_factory
from .colour import Colour
from .invite import Invite
from .mixins import Hashable
from .object import Object
from .partial_emoji import PartialEmoji
from .permissions import PermissionOverwrite, Permissions
from .threads import ForumTag, Thread
__all__ = (
"AuditLogDiff",
"AuditLogChanges",
"AuditLogEntry",
)
if TYPE_CHECKING:
import datetime
from .app_commands import APIApplicationCommand
from .automod import AutoModRule
from .emoji import Emoji
from .guild import Guild
from .guild_scheduled_event import GuildScheduledEvent
from .integrations import PartialIntegration
from .member import Member
from .role import Role
from .stage_instance import StageInstance
from .sticker import GuildSticker
from .types.audit_log import (
AuditLogChange as AuditLogChangePayload,
AuditLogEntry as AuditLogEntryPayload,
_AuditLogChange_ApplicationCommandPermissions as AuditLogChangeAppCmdPermsPayload,
)
from .types.automod import (
AutoModAction as AutoModActionPayload,
AutoModTriggerMetadata as AutoModTriggerMetadataPayload,
)
from .types.channel import (
DefaultReaction as DefaultReactionPayload,
PermissionOverwrite as PermissionOverwritePayload,
)
from .types.invite import Invite as InvitePayload
from .types.role import Role as RolePayload
from .types.snowflake import Snowflake
from .types.threads import ForumTag as ForumTagPayload
from .user import User
from .webhook import Webhook
def _transform_permissions(entry: AuditLogEntry, data: str) -> Permissions:
return Permissions(int(data))
def _transform_color(entry: AuditLogEntry, data: int) -> Colour:
return Colour(data)
def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int:
return int(data)
def _transform_channel(
entry: AuditLogEntry, data: Optional[Snowflake]
) -> Optional[Union[abc.GuildChannel, Object]]:
if data is None:
return None
channel = entry.guild.get_channel(int(data))
return channel or Object(id=data)
def _transform_role(
entry: AuditLogEntry, data: Optional[Snowflake]
) -> Optional[Union[Role, Object]]:
if data is None:
return None
role = entry.guild.get_role(int(data))
return role or Object(id=data)
def _transform_member_id(
entry: AuditLogEntry, data: Optional[Snowflake]
) -> Union[Member, User, Object, None]:
if data is None:
return None
return entry._get_member(int(data))
def _transform_guild_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Guild]:
if data is None:
return None
return entry._state._get_guild(int(data))
def _transform_overwrites(
entry: AuditLogEntry, data: List[PermissionOverwritePayload]
) -> List[Tuple[Union[Object, Member, Role, User], PermissionOverwrite]]:
overwrites: List[Tuple[Union[Object, Member, Role, User], PermissionOverwrite]] = []
for elem in data:
allow = Permissions(int(elem["allow"]))
deny = Permissions(int(elem["deny"]))
ow = PermissionOverwrite.from_pair(allow, deny)
ow_type = elem["type"]
ow_id = int(elem["id"])
target = None
if ow_type == 0:
target = entry.guild.get_role(ow_id)
elif ow_type == 1:
target = entry._get_member(ow_id)
if target is None:
target = Object(id=ow_id)
overwrites.append((target, ow))
return overwrites
def _transform_icon(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
if entry.action.name.startswith("role_"):
return Asset._from_role_icon(entry._state, entry._target_id, data) # type: ignore
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
def _transform_avatar(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore
def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]], Optional[Asset]]:
def _transform(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
return _transform
def _transform_tag(entry: AuditLogEntry, data: Optional[ForumTagPayload]) -> Optional[ForumTag]:
if data is None:
return None
return ForumTag._from_data(data=data, state=entry._state)
def _transform_tag_id(
entry: AuditLogEntry, data: Optional[str]
) -> Optional[Union[ForumTag, Object]]:
if data is None:
return None
# cyclic imports
from .channel import ThreadOnlyGuildChannel
tag: Optional[ForumTag] = None
tag_id = int(data)
thread = entry.target
# try thread parent first
if isinstance(thread, Thread) and isinstance(thread.parent, ThreadOnlyGuildChannel):
tag = thread.parent.get_tag(tag_id)
else:
# if not found (possibly deleted thread), search all forum/media channels
for channel in entry.guild._channels.values():
if isinstance(channel, ThreadOnlyGuildChannel) and (tag := channel.get_tag(tag_id)):
break
return tag or Object(id=tag_id)
T = TypeVar("T")
EnumT = TypeVar("EnumT", bound=enums.Enum)
FlagsT = TypeVar("FlagsT", bound=flags.BaseFlags)
def _enum_transformer(enum: Type[EnumT]) -> Callable[[AuditLogEntry, int], EnumT]:
def _transform(entry: AuditLogEntry, data: int) -> EnumT:
return enums.try_enum(enum, data)
return _transform
def _flags_transformer(
flags_type: Type[FlagsT],
) -> Callable[[AuditLogEntry, Optional[int]], Optional[FlagsT]]:
def _transform(entry: AuditLogEntry, data: Optional[int]) -> Optional[FlagsT]:
return flags_type._from_value(data) if data is not None else None
return _transform
def _list_transformer(
func: Callable[[AuditLogEntry, Any], T],
) -> Callable[[AuditLogEntry, Any], List[T]]:
def _transform(entry: AuditLogEntry, data: Any) -> List[T]:
if not data:
return []
return [func(entry, value) for value in data if value is not None]
return _transform
def _transform_type(
entry: AuditLogEntry, data: Any
) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str, int]:
action_name = entry.action.name
if action_name.startswith("sticker_"):
return enums.try_enum(enums.StickerType, data)
elif action_name.startswith("webhook_"):
return enums.try_enum(enums.WebhookType, data)
elif action_name.startswith("integration_") or action_name.startswith("overwrite_"):
# integration: str, overwrite: int
return data
else:
return enums.try_enum(enums.ChannelType, data)
def _transform_datetime(entry: AuditLogEntry, data: Optional[str]) -> Optional[datetime.datetime]:
return utils.parse_time(data)
def _transform_privacy_level(
entry: AuditLogEntry, data: Optional[int]
) -> Optional[Union[enums.StagePrivacyLevel, enums.GuildScheduledEventPrivacyLevel]]:
if data is None:
return None
if entry.action.target_type == "guild_scheduled_event":
return enums.try_enum(enums.GuildScheduledEventPrivacyLevel, data)
return enums.try_enum(enums.StagePrivacyLevel, data)
def _transform_guild_scheduled_event_image(
entry: AuditLogEntry, data: Optional[str]
) -> Optional[Asset]:
if data is None:
return None
return Asset._from_guild_scheduled_event_image(entry._state, entry._target_id, data) # type: ignore
def _transform_automod_action(
entry: AuditLogEntry, data: Optional[AutoModActionPayload]
) -> Optional[AutoModAction]:
if data is None:
return None
return _automod_action_factory(data)
def _transform_automod_trigger_metadata(
entry: AuditLogEntry, data: Optional[AutoModTriggerMetadataPayload]
) -> Optional[AutoModTriggerMetadata]:
if data is None:
return None
return AutoModTriggerMetadata._from_dict(data)
def _transform_default_reaction(
entry: AuditLogEntry, data: Optional[DefaultReactionPayload]
) -> Optional[Union[Emoji, PartialEmoji]]:
if data is None:
return None
return entry._state._get_emoji_from_fields(
name=data.get("emoji_name"),
id=utils._get_as_snowflake(data, "emoji_id"),
)
class AuditLogDiff:
def __len__(self) -> int:
return len(self.__dict__)
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
yield from self.__dict__.items()
def __repr__(self) -> str:
values = " ".join(f"{k!s}={v!r}" for k, v in self.__dict__.items())
return f"<AuditLogDiff {values}>"
if TYPE_CHECKING:
def __getattr__(self, item: str) -> Any: ...
def __setattr__(self, key: str, value: Any) -> Any: ...
Transformer = Callable[["AuditLogEntry", Any], Any]
class AuditLogChanges:
# fmt: off
TRANSFORMERS: ClassVar[Dict[str, Tuple[Optional[str], Optional[Transformer]]]] = {
"verification_level": (None, _enum_transformer(enums.VerificationLevel)),
"explicit_content_filter": (None, _enum_transformer(enums.ContentFilter)),
"allow": (None, _transform_permissions),
"deny": (None, _transform_permissions),
"permissions": (None, _transform_permissions),
"id": (None, _transform_snowflake),
"application_id": (None, _transform_snowflake),
"color": ("colour", _transform_color),
"owner_id": ("owner", _transform_member_id),
"inviter_id": ("inviter", _transform_member_id),
"channel_id": ("channel", _transform_channel),
"afk_channel_id": ("afk_channel", _transform_channel),
"system_channel_id": ("system_channel", _transform_channel),
"widget_channel_id": ("widget_channel", _transform_channel),
"rules_channel_id": ("rules_channel", _transform_channel),
"public_updates_channel_id": ("public_updates_channel", _transform_channel),
"permission_overwrites": ("overwrites", _transform_overwrites),
"splash_hash": ("splash", _guild_hash_transformer("splashes")),
"banner_hash": ("banner", _guild_hash_transformer("banners")),
"discovery_splash_hash": ("discovery_splash", _guild_hash_transformer("discovery-splashes")),
"icon_hash": ("icon", _transform_icon),
"avatar_hash": ("avatar", _transform_avatar),
"rate_limit_per_user": ("slowmode_delay", None),
"default_thread_rate_limit_per_user": ("default_thread_slowmode_delay", None),
"guild_id": ("guild", _transform_guild_id),
"tags": ("emoji", None),
"unicode_emoji": ("emoji", None),
"default_message_notifications": ("default_notifications", _enum_transformer(enums.NotificationLevel)),
"communication_disabled_until": ("timeout", _transform_datetime),
"image_hash": ("image", _transform_guild_scheduled_event_image),
"video_quality_mode": (None, _enum_transformer(enums.VideoQualityMode)),
"preferred_locale": (None, _enum_transformer(enums.Locale)),
"privacy_level": (None, _transform_privacy_level),
"format_type": (None, _enum_transformer(enums.StickerFormatType)),
"entity_type": (None, _enum_transformer(enums.GuildScheduledEventEntityType)),
"status": (None, _enum_transformer(enums.GuildScheduledEventStatus)),
"type": (None, _transform_type),
"flags": (None, _flags_transformer(flags.ChannelFlags)),
"system_channel_flags": (None, _flags_transformer(flags.SystemChannelFlags)),
"trigger_type": (None, _enum_transformer(enums.AutoModTriggerType)),
"event_type": (None, _enum_transformer(enums.AutoModEventType)),
"actions": (None, _list_transformer(_transform_automod_action)),
"trigger_metadata": (None, _transform_automod_trigger_metadata),
"exempt_roles": (None, _list_transformer(_transform_role)),
"exempt_channels": (None, _list_transformer(_transform_channel)),
"applied_tags": (None, _list_transformer(_transform_tag_id)),
"available_tags": (None, _list_transformer(_transform_tag)),
"default_reaction_emoji": ("default_reaction", _transform_default_reaction),
"default_sort_order": (None, _enum_transformer(enums.ThreadSortOrder)),
"sound_id": ("id", _transform_snowflake),
}
# fmt: on
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]) -> None:
self.before = AuditLogDiff()
self.after = AuditLogDiff()
has_emoji_fields = False
for elem in data:
attr = elem["key"]
# special cases for role add/remove
if attr == "$add":
self._handle_role(self.before, self.after, entry, elem["new_value"]) # type: ignore
continue
elif attr == "$remove":
self._handle_role(self.after, self.before, entry, elem["new_value"]) # type: ignore
continue
# special case for application command permissions update
if entry.action == enums.AuditLogAction.application_command_permission_update:
self._handle_command_permissions(
entry, cast("AuditLogChangeAppCmdPermsPayload", elem)
)
continue
# special case for flat emoji fields (discord, why), these will be merged later
if attr == "emoji_id" or attr == "emoji_name":
has_emoji_fields = True
transformer: Optional[Transformer]
try:
key, transformer = self.TRANSFORMERS[attr]
except (ValueError, KeyError):
transformer = None
else:
if key:
attr = key
try:
before = elem["old_value"]
except KeyError:
before = None
else:
if transformer:
before = transformer(entry, before)
setattr(self.before, attr, before)
try:
after = elem["new_value"]
except KeyError:
after = None
else:
if transformer:
after = transformer(entry, after)
setattr(self.after, attr, after)
if has_emoji_fields:
self._merge_emoji(entry)
# add an alias
if hasattr(self.after, "colour"):
self.after.color = self.after.colour
self.before.color = self.before.colour
if hasattr(self.after, "expire_behavior"):
self.after.expire_behaviour = self.after.expire_behavior
self.before.expire_behaviour = self.before.expire_behavior
def __repr__(self) -> str:
return f"<AuditLogChanges before={self.before!r} after={self.after!r}>"
def _handle_role(
self,
first: AuditLogDiff,
second: AuditLogDiff,
entry: AuditLogEntry,
elem: List[RolePayload],
) -> None:
if not hasattr(first, "roles"):
first.roles = []
data = []
g: Guild = entry.guild
for e in elem:
role_id = int(e["id"])
role = g.get_role(role_id)
if role is None:
role = Object(id=role_id)
role.name = e["name"] # type: ignore
data.append(role)
second.roles = data
def _handle_command_permissions(
self,
entry: AuditLogEntry,
data: AuditLogChangeAppCmdPermsPayload,
) -> None:
guild_id = entry.guild.id
entity_id = int(data["key"])
if not hasattr(self.before, "command_permissions"):
self.before.command_permissions = {}
if (old := data.get("old_value")) is not None:
self.before.command_permissions[entity_id] = ApplicationCommandPermissions(
data=old, guild_id=guild_id
)
if not hasattr(self.after, "command_permissions"):
self.after.command_permissions = {}
if (new := data.get("new_value")) is not None:
self.after.command_permissions[entity_id] = ApplicationCommandPermissions(
data=new, guild_id=guild_id
)
def _merge_emoji(self, entry: AuditLogEntry) -> None:
for diff in (self.before, self.after):
emoji_id: Optional[str] = diff.__dict__.pop("emoji_id", None)
emoji_name: Optional[str] = diff.__dict__.pop("emoji_name", None)
diff.emoji = entry._state._get_emoji_from_fields(
name=emoji_name,
id=int(emoji_id) if emoji_id else None,
)
class _AuditLogProxyMemberPrune:
delete_member_days: int
members_removed: int
class _AuditLogProxyMemberMoveOrMessageDelete:
channel: Union[abc.GuildChannel, Thread]
count: int
class _AuditLogProxyMemberDisconnect:
count: int
class _AuditLogProxyPinAction:
channel: Union[abc.GuildChannel, Thread]
message_id: int
class _AuditLogProxyStageInstanceAction:
channel: abc.GuildChannel
class _AuditLogProxyAutoModAction:
channel: Optional[Union[abc.GuildChannel, Thread]]
rule_name: str
rule_trigger_type: enums.AutoModTriggerType
class _AuditLogProxyKickOrMemberRoleAction:
integration_type: Optional[str]
class AuditLogEntry(Hashable):
"""Represents an Audit Log entry.
You can retrieve these via :meth:`Guild.audit_logs`,
or via the :func:`on_audit_log_entry_create` event.
.. collapse:: operations
.. describe:: x == y
Checks if two entries are equal.
.. describe:: x != y
Checks if two entries are not equal.
.. describe:: hash(x)
Returns the entry's hash.
.. versionchanged:: 1.7
Audit log entries are now comparable and hashable.
Attributes
----------
action: :class:`AuditLogAction`
The action that was done.
user: Optional[Union[:class:`Member`, :class:`User`, :class:`Object`]]
The user who initiated this action. Usually :class:`Member`\\, unless gone
then it's a :class:`User`.
.. versionchanged:: 2.8
May now be an :class:`Object` if the user could not be found.
id: :class:`int`
The entry ID.
target: Any
The target that got changed. The exact type of this depends on
the action being done.
extra: Any
Extra information that this entry has that might be useful.
For most actions, this is ``None``. However in some cases it
contains extra information. See :class:`AuditLogAction` for
which actions have this field filled out.
reason: Optional[:class:`str`]
The reason this action was done.
"""
def __init__(
self,
*,
data: AuditLogEntryPayload,
guild: Guild,
application_commands: Mapping[int, APIApplicationCommand],
automod_rules: Mapping[int, AutoModRule],
guild_scheduled_events: Mapping[int, GuildScheduledEvent],
integrations: Mapping[int, PartialIntegration],
threads: Mapping[int, Thread],
users: Mapping[int, User],
webhooks: Mapping[int, Webhook],
) -> None:
self._state = guild._state
self.guild = guild
self._application_commands = application_commands
self._automod_rules = automod_rules
self._guild_scheduled_events = guild_scheduled_events
self._integrations = integrations
self._threads = threads
self._users = users
self._webhooks = webhooks
self._from_data(data)
def _from_data(self, data: AuditLogEntryPayload) -> None:
self.action = enums.try_enum(enums.AuditLogAction, data["action_type"])
self.id = int(data["id"])
# this key is technically not usually present
self.reason = data.get("reason")
self.extra = extra = data.get("options")
if isinstance(self.action, enums.AuditLogAction) and extra:
if self.action is enums.AuditLogAction.member_prune:
elems = {
"delete_member_days": utils._get_as_snowflake(extra, "delete_member_days"),
"members_removed": utils._get_as_snowflake(extra, "members_removed"),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif (
self.action is enums.AuditLogAction.member_move
or self.action is enums.AuditLogAction.message_delete
):
elems = {
"count": int(extra["count"]),
"channel": self._get_channel_or_thread(
utils._get_as_snowflake(extra, "channel_id")
),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action is enums.AuditLogAction.member_disconnect:
elems = {
"count": int(extra["count"]),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action.name.endswith("pin"):
elems = {
"channel": self._get_channel_or_thread(
utils._get_as_snowflake(extra, "channel_id")
),
"message_id": int(extra["message_id"]),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action.name.startswith("overwrite_"):
instance_id = int(extra["id"])
the_type = extra.get("type")
if the_type == "1":
self.extra = self._get_member(instance_id)
elif the_type == "0":
role = self.guild.get_role(instance_id)
if role is None:
role = Object(id=instance_id)
role.name = extra.get("role_name") # type: ignore
self.extra = role
elif self.action.name.startswith("stage_instance"):
elems = {
"channel": self._get_channel_or_thread(
utils._get_as_snowflake(extra, "channel_id")
)
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action is enums.AuditLogAction.application_command_permission_update:
app_id = int(extra["application_id"])
elems = {
"integration": self._get_integration_by_application_id(app_id) or Object(app_id)
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action in (
enums.AuditLogAction.automod_block_message,
enums.AuditLogAction.automod_send_alert_message,
enums.AuditLogAction.automod_timeout,
enums.AuditLogAction.automod_quarantine_user,
):
elems = {
"channel": (
self._get_channel_or_thread(utils._get_as_snowflake(extra, "channel_id"))
),
"rule_name": extra["auto_moderation_rule_name"],
"rule_trigger_type": enums.try_enum(
enums.AutoModTriggerType,
int(extra["auto_moderation_rule_trigger_type"]),
),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action in (
enums.AuditLogAction.kick,
enums.AuditLogAction.member_role_update,
):
elems = {
# unlike other extras, this key isn't always provided
"integration_type": extra.get("integration_type"),
}
self.extra = type("_AuditLogProxy", (), elems)()
self.extra: Any
# actually this but there's no reason to annoy users with this:
# Union[
# _AuditLogProxyMemberPrune,
# _AuditLogProxyMemberMoveOrMessageDelete,
# _AuditLogProxyMemberDisconnect,
# _AuditLogProxyPinAction,
# _AuditLogProxyStageInstanceAction,
# _AuditLogProxyAutoModAction,
# _AuditLogProxyKickOrMemberRoleAction,
# Member, User, None,
# Role,
# ]
# this key is not present when the above is present, typically.
# It's a list of { new_value: a, old_value: b, key: c }
# where new_value and old_value are not guaranteed to be there depending
# on the action type, so let's just fetch it for now and only turn it
# into meaningful data when requested
self._changes = data.get("changes", [])
self.user = self._get_member(utils._get_as_snowflake(data, "user_id"))
self._target_id = utils._get_as_snowflake(data, "target_id")
def _get_member(self, user_id: Optional[int]) -> Union[Member, User, Object, None]:
if not user_id:
return None
return self.guild.get_member(user_id) or self._users.get(user_id) or Object(id=user_id)
def _get_channel_or_thread(
self, channel_id: Optional[int]
) -> Union[abc.GuildChannel, Thread, Object, None]:
if not channel_id:
return None
return self.guild.get_channel_or_thread(channel_id) or Object(channel_id)
def _get_integration_by_application_id(
self, application_id: int
) -> Optional[PartialIntegration]:
if not application_id:
return None
for integration in self._integrations.values():
if integration.application_id == application_id:
return integration
return None
def __repr__(self) -> str:
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"
@utils.cached_property
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
return utils.snowflake_time(self.id)
@utils.cached_property
def target(
self,
) -> Union[
Guild,
abc.GuildChannel,
Member,
User,
Role,
Invite,
Webhook,
Emoji,
PartialIntegration,
StageInstance,
GuildSticker,
Thread,
GuildScheduledEvent,
APIApplicationCommand,
AutoModRule,
Object,
None,
]:
if self.action.target_type is None:
return Object(id=self._target_id) if self._target_id else None
try:
converter = getattr(self, f"_convert_target_{self.action.target_type}")
except AttributeError:
return Object(id=self._target_id) if self._target_id else None
else:
return converter(self._target_id)
@utils.cached_property
def category(self) -> Optional[enums.AuditLogActionCategory]:
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
return self.action.category
@utils.cached_property
def changes(self) -> AuditLogChanges:
""":class:`AuditLogChanges`: The list of changes this entry has."""
obj = AuditLogChanges(self, self._changes)
del self._changes
return obj
@utils.cached_property
def before(self) -> AuditLogDiff:
""":class:`AuditLogDiff`: The target's prior state."""
return self.changes.before
@utils.cached_property
def after(self) -> AuditLogDiff:
""":class:`AuditLogDiff`: The target's subsequent state."""
return self.changes.after
def _convert_target_guild(self, target_id: int) -> Guild:
return self.guild
def _convert_target_channel(self, target_id: int) -> Union[abc.GuildChannel, Object]:
return self.guild.get_channel(target_id) or Object(id=target_id)
def _convert_target_user(self, target_id: int) -> Union[Member, User, Object, None]:
return self._get_member(target_id)
def _convert_target_role(self, target_id: int) -> Union[Role, Object]:
return self.guild.get_role(target_id) or Object(id=target_id)
def _convert_target_invite(self, target_id: int) -> Invite:
# invites have target_id set to null
# so figure out which change has the full invite data
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
fake_payload: InvitePayload = {
"max_age": changeset.max_age,
"max_uses": changeset.max_uses,
"code": changeset.code,
"temporary": changeset.temporary,
"uses": changeset.uses,
"type": 0,
"channel": None,
"expires_at": None,
}
obj = Invite(
state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel
)
try:
obj.inviter = changeset.inviter
except AttributeError:
pass
return obj
def _convert_target_webhook(self, target_id: int) -> Union[Webhook, Object]:
return self._webhooks.get(target_id) or Object(id=target_id)
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
return self._state.get_emoji(target_id) or Object(id=target_id)
def _convert_target_message(self, target_id: int) -> Union[Member, User, Object, None]:
return self._get_member(target_id)
def _convert_target_integration(self, target_id: int) -> Union[PartialIntegration, Object]:
return self._integrations.get(target_id) or Object(id=target_id)
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:
return self.guild.get_stage_instance(target_id) or Object(id=target_id)
def _convert_target_sticker(self, target_id: int) -> Union[GuildSticker, Object]:
return self._state.get_sticker(target_id) or Object(id=target_id)
def _convert_target_thread(self, target_id: int) -> Union[Thread, Object]:
return (
self.guild.get_thread(target_id) or self._threads.get(target_id) or Object(id=target_id)
)
def _convert_target_guild_scheduled_event(
self, target_id: int
) -> Union[GuildScheduledEvent, Object]:
return (
self.guild.get_scheduled_event(target_id)
or self._guild_scheduled_events.get(target_id)
or Object(id=target_id)
)
def _convert_target_application_command_or_integration(
self, target_id: int
) -> Union[APIApplicationCommand, PartialIntegration, Object]:
# try application command
if target := (
self._state._get_guild_application_command(self.guild.id, target_id)
or self._state._get_global_application_command(target_id)
or self._application_commands.get(target_id)
):
return target
# permissions may also be changed for the entire application,
# however the target ID is the application ID, not the integration ID
if target := self._get_integration_by_application_id(target_id):
return target
# fall back to object
return Object(id=target_id)
def _convert_target_automod_rule(self, target_id: int) -> Union[AutoModRule, Object]:
return self._automod_rules.get(target_id) or Object(id=target_id)

View File

@@ -0,0 +1,803 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import datetime
from typing import (
TYPE_CHECKING,
Dict,
FrozenSet,
Iterable,
List,
Optional,
Sequence,
Type,
Union,
overload,
)
from .enums import (
AutoModActionType,
AutoModEventType,
AutoModTriggerType,
enum_if_int,
try_enum,
try_enum_to_int,
)
from .flags import AutoModKeywordPresets
from .utils import MISSING, _get_as_snowflake, snowflake_time
if TYPE_CHECKING:
from typing_extensions import Self
from .abc import Snowflake
from .guild import Guild, GuildChannel
from .member import Member
from .message import Message
from .role import Role
from .threads import Thread
from .types.automod import (
AutoModAction as AutoModActionPayload,
AutoModActionMetadata,
AutoModBlockMessageActionMetadata,
AutoModRule as AutoModRulePayload,
AutoModSendAlertActionMetadata,
AutoModTimeoutActionMetadata,
AutoModTriggerMetadata as AutoModTriggerMetadataPayload,
EditAutoModRule as EditAutoModRulePayload,
)
from .types.gateway import AutoModerationActionExecutionEvent
__all__ = (
"AutoModAction",
"AutoModBlockMessageAction",
"AutoModSendAlertAction",
"AutoModTimeoutAction",
"AutoModTriggerMetadata",
"AutoModRule",
"AutoModActionExecution",
)
class AutoModAction:
"""A base class for auto moderation actions.
This class is not meant to be instantiated by the user.
The user-constructible subclasses are:
- :class:`AutoModBlockMessageAction`
- :class:`AutoModSendAlertAction`
- :class:`AutoModTimeoutAction`
Actions received from the API may be of this type
(and not one of the subtypes above) if the action type is not implemented yet.
.. versionadded:: 2.6
Attributes
----------
type: :class:`AutoModActionType`
The action type.
"""
__slots__ = ("type", "_metadata")
def __init__(
self,
*,
type: AutoModActionType,
) -> None:
self.type: AutoModActionType = enum_if_int(AutoModActionType, type)
self._metadata: AutoModActionMetadata = {}
def __repr__(self) -> str:
return f"<{type(self).__name__} type={self.type!r}>"
@classmethod
def _from_dict(cls, data: AutoModActionPayload) -> Self:
self = cls.__new__(cls)
self.type = try_enum(AutoModActionType, data["type"])
self._metadata = data.get("metadata", {})
return self
def to_dict(self) -> AutoModActionPayload:
return {
"type": self.type.value,
"metadata": self._metadata,
}
class AutoModBlockMessageAction(AutoModAction):
"""Represents an auto moderation action that blocks content from being sent.
.. versionadded:: 2.6
Parameters
----------
custom_message: Optional[:class:`str`]
The custom message to show to the user when the rule is triggered.
Maximum length is 150 characters.
.. versionadded:: 2.9
Attributes
----------
type: :class:`AutoModActionType`
The action type.
Always set to :attr:`~AutoModActionType.block_message`.
"""
__slots__ = ()
_metadata: AutoModBlockMessageActionMetadata
def __init__(self, custom_message: Optional[str] = None) -> None:
super().__init__(type=AutoModActionType.block_message)
if custom_message is not None:
self._metadata["custom_message"] = custom_message
@property
def custom_message(self) -> Optional[str]:
"""Optional[:class:`str`]: The custom message to show to the user when the rule is triggered.
.. versionadded:: 2.9
"""
return self._metadata.get("custom_message")
def __repr__(self) -> str:
return f"<{type(self).__name__} custom_message={self.custom_message!r}>"
class AutoModSendAlertAction(AutoModAction):
"""Represents an auto moderation action that sends an alert to a channel.
.. versionadded:: 2.6
Parameters
----------
channel: :class:`abc.Snowflake`
The channel to send an alert in when the rule is triggered.
Attributes
----------
type: :class:`AutoModActionType`
The action type.
Always set to :attr:`~AutoModActionType.send_alert_message`.
"""
__slots__ = ()
_metadata: AutoModSendAlertActionMetadata
def __init__(self, channel: Snowflake) -> None:
super().__init__(type=AutoModActionType.send_alert_message)
self._metadata["channel_id"] = channel.id
@property
def channel_id(self) -> int:
""":class:`int`: The channel ID to send an alert in when the rule is triggered."""
return int(self._metadata["channel_id"])
def __repr__(self) -> str:
return f"<{type(self).__name__} channel_id={self.channel_id!r}>"
class AutoModTimeoutAction(AutoModAction):
"""Represents an auto moderation action that times out the user.
.. versionadded:: 2.6
Parameters
----------
duration: Union[:class:`int`, :class:`datetime.timedelta`]
The duration (seconds or timedelta) for which to timeout the user when the rule is triggered.
Attributes
----------
type: :class:`AutoModActionType`
The action type.
Always set to :attr:`~AutoModActionType.timeout`.
"""
__slots__ = ()
_metadata: AutoModTimeoutActionMetadata
def __init__(self, duration: Union[int, datetime.timedelta]) -> None:
super().__init__(type=AutoModActionType.timeout)
if isinstance(duration, datetime.timedelta):
duration = int(duration.total_seconds())
self._metadata["duration_seconds"] = duration
@property
def duration(self) -> int:
""":class:`int`: The duration (in seconds) for which to timeout
the user when the rule is triggered.
"""
return self._metadata["duration_seconds"]
def __repr__(self) -> str:
return f"<{type(self).__name__} duration={self.duration!r}>"
class AutoModTriggerMetadata:
"""Metadata for an auto moderation trigger.
Based on the trigger type, different fields can be used with various limits:
.. csv-table::
:header: "Trigger Type", ``keyword_filter``, ``regex_patterns``, ``presets``, ``allow_list``, ``mention_total_limit``, ``mention_raid_protection_enabled``
:attr:`~AutoModTriggerType.keyword`, ✅ (x1000), ✅ (x10), ❌, ✅ (x100), ❌, ❌
:attr:`~AutoModTriggerType.spam`, ❌, ❌, ❌, ❌, ❌, ❌
:attr:`~AutoModTriggerType.keyword_preset`, ❌, ❌, ✅, ✅ (x1000), ❌, ❌
:attr:`~AutoModTriggerType.mention_spam`, ❌, ❌, ❌, ❌, ✅, ✅
.. versionadded:: 2.6
Attributes
----------
keyword_filter: Optional[Sequence[:class:`str`]]
The list of keywords to check for, up to 1000 keywords. Used with :attr:`AutoModTriggerType.keyword`.
See :ddocs:`api docs <resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies>`
for details about how keyword matching works.
Each keyword must be 60 characters or less.
regex_patterns: Optional[Sequence[:class:`str`]]
The list of regular expressions to check for. Used with :attr:`AutoModTriggerType.keyword`.
A maximum of 10 regexes can be added, each with up to 260 characters.
.. note::
Only Rust flavored regex is currently supported, which can be tested in online editors such as `Rustexp <https://rustexp.lpil.uk/>`__.
.. versionadded:: 2.7
presets: Optional[:class:`AutoModKeywordPresets`]
The keyword presets. Used with :attr:`AutoModTriggerType.keyword_preset`.
allow_list: Optional[Sequence[:class:`str`]]
The keywords that should be exempt from a preset.
Used with :attr:`AutoModTriggerType.keyword` (up to 100 exemptions) and :attr:`AutoModTriggerType.keyword_preset` (up to 1000 exemptions).
Each keyword must be 60 characters or less.
mention_total_limit: Optional[:class:`int`]
The maximum number of mentions (members + roles) allowed, between 1 and 50. Used with :attr:`AutoModTriggerType.mention_spam`.
mention_raid_protection_enabled: Optional[:class:`bool`]
Whether to automatically detect mention raids. Used with :attr:`AutoModTriggerType.mention_spam`.
Defaults to ``False``.
.. versionadded:: 2.9
"""
__slots__ = (
"keyword_filter",
"regex_patterns",
"presets",
"allow_list",
"mention_total_limit",
"mention_raid_protection_enabled",
)
@overload
def __init__(
self,
*,
keyword_filter: Optional[Sequence[str]],
regex_patterns: Optional[Sequence[str]] = None,
allow_list: Optional[Sequence[str]] = None,
) -> None: ...
@overload
def __init__(
self,
*,
keyword_filter: Optional[Sequence[str]] = None,
regex_patterns: Optional[Sequence[str]],
allow_list: Optional[Sequence[str]] = None,
) -> None: ...
@overload
def __init__(
self,
*,
presets: AutoModKeywordPresets,
allow_list: Optional[Sequence[str]] = None,
) -> None: ...
@overload
def __init__(
self, *, mention_total_limit: int, mention_raid_protection_enabled: bool = False
) -> None: ...
def __init__(
self,
*,
keyword_filter: Optional[Sequence[str]] = None,
regex_patterns: Optional[Sequence[str]] = None,
presets: Optional[AutoModKeywordPresets] = None,
allow_list: Optional[Sequence[str]] = None,
mention_total_limit: Optional[int] = None,
mention_raid_protection_enabled: Optional[bool] = None,
) -> None:
self.keyword_filter: Optional[Sequence[str]] = keyword_filter
self.regex_patterns: Optional[Sequence[str]] = regex_patterns
self.presets: Optional[AutoModKeywordPresets] = presets
self.allow_list: Optional[Sequence[str]] = allow_list
self.mention_total_limit: Optional[int] = mention_total_limit
self.mention_raid_protection_enabled: Optional[bool] = mention_raid_protection_enabled
def with_changes(
self,
*,
keyword_filter: Optional[Sequence[str]] = MISSING,
regex_patterns: Optional[Sequence[str]] = MISSING,
presets: Optional[AutoModKeywordPresets] = MISSING,
allow_list: Optional[Sequence[str]] = MISSING,
mention_total_limit: Optional[int] = MISSING,
mention_raid_protection_enabled: Optional[bool] = MISSING,
) -> Self:
"""Returns a new instance with the given changes applied.
All other fields will be kept intact.
Returns
-------
:class:`AutoModTriggerMetadata`
The new metadata instance.
"""
return self.__class__( # type: ignore # call doesn't match any overloads
keyword_filter=self.keyword_filter if keyword_filter is MISSING else keyword_filter,
regex_patterns=self.regex_patterns if regex_patterns is MISSING else regex_patterns,
presets=self.presets if presets is MISSING else presets,
allow_list=self.allow_list if allow_list is MISSING else allow_list,
mention_total_limit=(
self.mention_total_limit if mention_total_limit is MISSING else mention_total_limit
),
mention_raid_protection_enabled=(
self.mention_raid_protection_enabled
if mention_raid_protection_enabled is MISSING
else mention_raid_protection_enabled
),
)
@classmethod
def _from_dict(cls, data: AutoModTriggerMetadataPayload) -> Self:
if (presets_data := data.get("presets")) is not None:
presets = AutoModKeywordPresets._from_values(presets_data)
else:
presets = None
return cls( # type: ignore # call doesn't match any overloads
keyword_filter=data.get("keyword_filter"),
regex_patterns=data.get("regex_patterns"),
presets=presets,
allow_list=data.get("allow_list"),
mention_total_limit=data.get("mention_total_limit"),
mention_raid_protection_enabled=data.get("mention_raid_protection_enabled"),
)
def to_dict(self) -> AutoModTriggerMetadataPayload:
data: AutoModTriggerMetadataPayload = {}
if self.keyword_filter is not None:
data["keyword_filter"] = list(self.keyword_filter)
if self.regex_patterns is not None:
data["regex_patterns"] = list(self.regex_patterns)
if self.presets is not None:
data["presets"] = self.presets.values # type: ignore # `values` contains ints instead of preset literal values
if self.allow_list is not None:
data["allow_list"] = list(self.allow_list)
if self.mention_total_limit is not None:
data["mention_total_limit"] = self.mention_total_limit
if self.mention_raid_protection_enabled is not None:
data["mention_raid_protection_enabled"] = self.mention_raid_protection_enabled
return data
def __repr__(self) -> str:
s = f"<{type(self).__name__}"
if self.keyword_filter is not None:
s += f" keyword_filter={self.keyword_filter!r}"
if self.regex_patterns is not None:
s += f" regex_patterns={self.regex_patterns!r}"
if self.presets is not None:
s += f" presets={self.presets!r}"
if self.allow_list is not None:
s += f" allow_list={self.allow_list!r}"
if self.mention_total_limit is not None:
s += f" mention_total_limit={self.mention_total_limit!r}"
if self.mention_raid_protection_enabled is not None:
s += f" mention_raid_protection_enabled={self.mention_raid_protection_enabled!r}"
return f"{s}>"
class AutoModRule:
"""Represents an auto moderation rule.
.. versionadded:: 2.6
Attributes
----------
id: :class:`int`
The rule ID.
name: :class:`str`
The rule name.
enabled: :class:`bool`
Whether this rule is enabled.
guild: :class:`Guild`
The guild of the rule.
creator_id: :class:`int`
The rule creator's ID. See also :attr:`.creator`.
event_type: :class:`AutoModEventType`
The event type this rule is applied to.
trigger_type: :class:`AutoModTriggerType`
The type of trigger that determines whether this rule's actions should run for a specific event.
trigger_metadata: :class:`AutoModTriggerMetadata`
Additional metadata associated with this rule's :attr:`.trigger_type`.
exempt_role_ids: FrozenSet[:class:`int`]
The role IDs that are exempt from this rule.
exempt_channel_ids: FrozenSet[:class:`int`]
The channel IDs that are exempt from this rule.
"""
__slots__ = (
"id",
"name",
"enabled",
"guild",
"creator_id",
"event_type",
"trigger_type",
"trigger_metadata",
"_actions",
"exempt_role_ids",
"exempt_channel_ids",
)
def __init__(self, *, data: AutoModRulePayload, guild: Guild) -> None:
self.guild: Guild = guild
self.id: int = int(data["id"])
self.name: str = data["name"]
self.enabled: bool = data["enabled"]
self.creator_id: int = int(data["creator_id"])
self.event_type: AutoModEventType = try_enum(AutoModEventType, data["event_type"])
self.trigger_type: AutoModTriggerType = try_enum(AutoModTriggerType, data["trigger_type"])
self._actions: List[AutoModAction] = [
_automod_action_factory(action) for action in data["actions"]
]
self.trigger_metadata: AutoModTriggerMetadata = AutoModTriggerMetadata._from_dict(
data.get("trigger_metadata", {})
)
self.exempt_role_ids: FrozenSet[int] = (
frozenset(map(int, exempt_roles))
if (exempt_roles := data.get("exempt_roles"))
else frozenset()
)
self.exempt_channel_ids: FrozenSet[int] = (
frozenset(map(int, exempt_channels))
if (exempt_channels := data.get("exempt_channels"))
else frozenset()
)
@property
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the rule's creation time in UTC.
.. versionadded:: 2.10
"""
return snowflake_time(self.id)
@property
def actions(self) -> List[AutoModAction]:
"""List[Union[:class:`AutoModBlockMessageAction`, :class:`AutoModSendAlertAction`, :class:`AutoModTimeoutAction`, :class:`AutoModAction`]]:
The list of actions that will execute if a matching event triggered this rule.
"""
return list(self._actions) # return a copy
@property
def creator(self) -> Optional[Member]:
"""Optional[:class:`Member`]: The guild member that created this rule.
May be ``None`` if the member cannot be found. See also :attr:`.creator_id`.
"""
return self.guild.get_member(self.creator_id)
@property
def exempt_roles(self) -> List[Role]:
"""List[:class:`Role`]: The list of roles that are exempt from this rule."""
return list(filter(None, map(self.guild.get_role, self.exempt_role_ids)))
@property
def exempt_channels(self) -> List[GuildChannel]:
"""List[:class:`abc.GuildChannel`]: The list of channels that are exempt from this rule."""
return list(filter(None, map(self.guild.get_channel, self.exempt_channel_ids)))
def __repr__(self) -> str:
return (
f"<AutoModRule id={self.id!r} name={self.name!r} enabled={self.enabled!r}"
f" creator={self.creator!r} event_type={self.event_type!r} trigger_type={self.trigger_type!r}"
f" actions={self._actions!r} exempt_roles={self.exempt_role_ids!r} exempt_channels={self.exempt_channel_ids!r}"
f" trigger_metadata={self.trigger_metadata!r}>"
)
async def edit(
self,
*,
name: str = MISSING,
event_type: AutoModEventType = MISSING,
trigger_metadata: AutoModTriggerMetadata = MISSING,
actions: Sequence[AutoModAction] = MISSING,
enabled: bool = MISSING,
exempt_roles: Optional[Iterable[Snowflake]] = MISSING,
exempt_channels: Optional[Iterable[Snowflake]] = MISSING,
reason: Optional[str] = None,
) -> AutoModRule:
"""|coro|
Edits the auto moderation rule.
You must have :attr:`.Permissions.manage_guild` permission to do this.
All fields are optional.
.. versionchanged:: 2.9
Now raises a :exc:`TypeError` if given ``actions`` have an invalid type.
Examples
--------
Edit name and enable rule:
.. code-block:: python3
await rule.edit(name="cool new rule", enabled=True)
Add an action:
.. code-block:: python3
await rule.edit(
actions=rule.actions + [AutoModTimeoutAction(3600)],
)
Add a keyword to a keyword filter rule:
.. code-block:: python3
meta = rule.trigger_metadata
await rule.edit(
trigger_metadata=meta.with_changes(
keyword_filter=meta.keyword_filter + ["stuff"],
),
)
Parameters
----------
name: :class:`str`
The rule's new name.
event_type: :class:`AutoModEventType`
The rule's new event type.
trigger_metadata: :class:`AutoModTriggerMetadata`
The rule's new associated trigger metadata.
actions: Sequence[Union[:class:`AutoModBlockMessageAction`, :class:`AutoModSendAlertAction`, :class:`AutoModTimeoutAction`, :class:`AutoModAction`]]
The rule's new actions.
If provided, must contain at least one action.
enabled: :class:`bool`
Whether to enable the rule.
exempt_roles: Optional[Iterable[:class:`abc.Snowflake`]]
The rule's new exempt roles, up to 20.
If ``[]`` or ``None`` is passed then all role exemptions are removed.
exempt_channels: Optional[Iterable[:class:`abc.Snowflake`]]
The rule's new exempt channels, up to 50.
Can also include categories, in which case all channels inside that category will be exempt.
If ``[]`` or ``None`` is passed then all channel exemptions are removed.
reason: Optional[:class:`str`]
The reason for editing the rule. Shows up on the audit log.
Raises
------
ValueError
When editing the list of actions, at least one action must be provided.
TypeError
The specified ``actions`` are of an invalid type.
Forbidden
You do not have proper permissions to edit the rule.
NotFound
The rule does not exist.
HTTPException
Editing the rule failed.
Returns
-------
:class:`AutoModRule`
The newly updated auto moderation rule.
"""
payload: EditAutoModRulePayload = {}
if name is not MISSING:
payload["name"] = name
if event_type is not MISSING:
payload["event_type"] = try_enum_to_int(event_type)
if trigger_metadata is not MISSING:
payload["trigger_metadata"] = trigger_metadata.to_dict()
if actions is not MISSING:
if not actions:
raise ValueError("At least one action must be provided.")
for action in actions:
if not isinstance(action, AutoModAction):
raise TypeError(
f"actions must be of type `AutoModAction` (or subtype), not {type(action)!r}"
)
payload["actions"] = [a.to_dict() for a in actions]
if enabled is not MISSING:
payload["enabled"] = enabled
if exempt_roles is not MISSING:
payload["exempt_roles"] = (
[e.id for e in exempt_roles] if exempt_roles is not None else []
)
if exempt_channels is not MISSING:
payload["exempt_channels"] = (
[e.id for e in exempt_channels] if exempt_channels is not None else []
)
data = await self.guild._state.http.edit_auto_moderation_rule(
self.guild.id, self.id, reason=reason, **payload
)
return AutoModRule(data=data, guild=self.guild)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the auto moderation rule.
You must have :attr:`.Permissions.manage_guild` permission to do this.
Parameters
----------
reason: Optional[:class:`str`]
The reason for deleting this rule. Shows up on the audit log.
Raises
------
Forbidden
You do not have proper permissions to delete the rule.
NotFound
The rule does not exist.
HTTPException
Deleting the rule failed.
"""
await self.guild._state.http.delete_auto_moderation_rule(
self.guild.id, self.id, reason=reason
)
class AutoModActionExecution:
"""Represents the data for an :func:`on_automod_action_execution` event.
.. versionadded:: 2.6
Attributes
----------
action: Union[:class:`AutoModBlockMessageAction`, :class:`AutoModSendAlertAction`, :class:`AutoModTimeoutAction`, :class:`AutoModAction`]
The action that was executed.
guild: :class:`Guild`
The guild this action was executed in.
rule_id: :class:`int`
The ID of the rule that matched.
rule_trigger_type: :class:`AutoModTriggerType`
The trigger type of the rule that matched.
user_id: :class:`int`
The ID of the user that triggered this action.
See also :attr:`.user`.
channel_id: Optional[:class:`int`]
The channel or thread ID in which the event occurred, if any.
See also :attr:`.channel`.
message_id: Optional[:class:`int`]
The ID of the message that matched. ``None`` if the message was blocked,
or if the content was not part of a message.
See also :attr:`.message`.
alert_message_id: Optional[:class:`int`]
The ID of the alert message sent as a result of this action, if any.
See also :attr:`.alert_message`.
content: :class:`str`
The content that matched.
Requires :attr:`Intents.message_content` to be enabled,
otherwise this field will be empty.
matched_keyword: Optional[:class:`str`]
The keyword or regex that matched.
matched_content: Optional[:class:`str`]
The substring of :attr:`.content` that matched the rule/keyword.
Requires :attr:`Intents.message_content` to be enabled,
otherwise this field will be empty.
"""
__slots__ = (
"action",
"guild",
"rule_id",
"rule_trigger_type",
"user_id",
"channel_id",
"message_id",
"alert_message_id",
"content",
"matched_keyword",
"matched_content",
)
def __init__(self, *, data: AutoModerationActionExecutionEvent, guild: Guild) -> None:
self.guild: Guild = guild
self.action: AutoModAction = _automod_action_factory(data["action"])
self.rule_id: int = int(data["rule_id"])
self.rule_trigger_type: AutoModTriggerType = try_enum(
AutoModTriggerType, data["rule_trigger_type"]
)
self.user_id: int = int(data["user_id"])
self.channel_id: Optional[int] = _get_as_snowflake(data, "channel_id")
self.message_id: Optional[int] = _get_as_snowflake(data, "message_id")
self.alert_message_id: Optional[int] = _get_as_snowflake(data, "alert_system_message_id")
self.content: str = data.get("content") or ""
self.matched_keyword: Optional[str] = data.get("matched_keyword")
self.matched_content: Optional[str] = data.get("matched_content")
def __repr__(self) -> str:
return (
f"<{type(self).__name__} guild={self.guild!r} action={self.action!r}"
f" rule_id={self.rule_id!r} rule_trigger_type={self.rule_trigger_type!r}"
f" channel={self.channel!r} user_id={self.user_id!r} message_id={self.message_id!r}"
f" alert_message_id={self.alert_message_id!r} content={self.content!r}"
f" matched_keyword={self.matched_keyword!r} matched_content={self.matched_content!r}>"
)
@property
def user(self) -> Optional[Member]:
"""Optional[:class:`Member`]: The guild member that triggered this action.
May be ``None`` if the member cannot be found. See also :attr:`.user_id`.
"""
return self.guild.get_member(self.user_id)
@property
def channel(self) -> Optional[Union[GuildChannel, Thread]]:
"""Optional[Union[:class:`abc.GuildChannel`, :class:`Thread`]]:
The channel or thread in which the event occurred, if any.
"""
return self.guild._resolve_channel(self.channel_id)
@property
def message(self) -> Optional[Message]:
"""Optional[:class:`Message`]: The message that matched, if any.
Not available if the message was blocked, if the content was not part of a message,
or if the message was not found in the message cache.
"""
return self.guild._state._get_message(self.message_id)
@property
def alert_message(self) -> Optional[Message]:
"""Optional[:class:`Message`]: The alert message sent as a result of this action, if any.
Only available if :attr:`action.type <AutoModAction.type>` is :attr:`~AutoModActionType.send_alert_message`
and the message was found in the message cache.
"""
return self.guild._state._get_message(self.alert_message_id)
_action_map: Dict[int, Type[AutoModAction]] = {
AutoModActionType.block_message.value: AutoModBlockMessageAction,
AutoModActionType.send_alert_message.value: AutoModSendAlertAction,
AutoModActionType.timeout.value: AutoModTimeoutAction,
}
def _automod_action_factory(data: AutoModActionPayload) -> AutoModAction:
tp = _action_map.get(data["type"], AutoModAction)
return tp._from_dict(data)

View File

@@ -0,0 +1,80 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import random
import time
from typing import Callable, Generic, Literal, TypeVar, Union, overload
T = TypeVar("T", bool, Literal[True], Literal[False])
__all__ = ("ExponentialBackoff",)
class ExponentialBackoff(Generic[T]):
"""An implementation of the exponential backoff algorithm
Provides a convenient interface to implement an exponential backoff
for reconnecting or retrying transmissions in a distributed network.
Once instantiated, the delay method will return the next interval to
wait for when retrying a connection or transmission. The maximum
delay increases exponentially with each retry up to a maximum of
2^10 * base, and is reset if no more attempts are needed in a period
of 2^11 * base seconds.
Parameters
----------
base: :class:`int`
The base delay in seconds. The first retry-delay will be up to
this many seconds.
integral: :class:`bool`
Set to ``True`` if whole periods of base is desirable, otherwise any
number in between may be returned.
"""
def __init__(self, base: int = 1, *, integral: T = False) -> None:
self._base: int = base
self._exp: int = 0
self._max: int = 10
self._reset_time: int = base * 2**11
self._last_invocation: float = time.monotonic()
# Use our own random instance to avoid messing with global one
rand = random.Random()
rand.seed()
self._randfunc: Callable[..., Union[int, float]] = (
rand.randrange if integral else rand.uniform
)
@overload
def delay(self: ExponentialBackoff[Literal[False]]) -> float: ...
@overload
def delay(self: ExponentialBackoff[Literal[True]]) -> int: ...
@overload
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]: ...
def delay(self) -> Union[int, float]:
"""Compute the next delay
Returns the next delay to wait according to the exponential
backoff algorithm. This is a value between 0 and base * 2^exp
where exponent starts off at 1 and is incremented at every
invocation of this method up to a maximum of 10.
If a period of more than base * 2^11 has passed since the last
retry, the exponent is reset to 1.
"""
invocation = time.monotonic()
interval = invocation - self._last_invocation
self._last_invocation = invocation
if interval > self._reset_time:
self._exp = 0
self._exp = min(self._exp + 1, self._max)
return self._randfunc(0, self._base * 2**self._exp)

View File

@@ -0,0 +1,21 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, NamedTuple, Optional, Sequence
__all__ = ("BanEntry",)
if TYPE_CHECKING:
from .abc import Snowflake
from .user import User
class BanEntry(NamedTuple):
reason: Optional[str]
user: "User"
class BulkBanResult(NamedTuple):
banned: Sequence[Snowflake]
failed: Sequence[Snowflake]

View File

@@ -0,0 +1,28 @@
Copyright (c) 1994-2013 Xiph.Org Foundation and contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.Org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,336 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import colorsys
import random
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = (
"Colour",
"Color",
)
class Colour:
"""Represents a Discord role colour. This class is similar
to a (red, green, blue) :class:`tuple`.
There is an alias for this called Color.
.. collapse:: operations
.. describe:: x == y
Checks if two colours are equal.
.. describe:: x != y
Checks if two colours are not equal.
.. describe:: hash(x)
Return the colour's hash.
.. describe:: str(x)
Returns the hex format for the colour.
.. describe:: int(x)
Returns the raw colour value.
Attributes
----------
value: :class:`int`
The raw integer colour value.
"""
__slots__ = ("value",)
def __init__(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(f"Expected int parameter, received {value.__class__.__name__} instead.")
self.value: int = value
def _get_byte(self, byte: int) -> int:
return (self.value >> (8 * byte)) & 0xFF
def __eq__(self, other: Any) -> bool:
return isinstance(other, Colour) and self.value == other.value
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __str__(self) -> str:
return f"#{self.value:0>6x}"
def __int__(self) -> int:
return self.value
def __repr__(self) -> str:
return f"<Colour value={self.value}>"
def __hash__(self) -> int:
return hash(self.value)
@property
def r(self) -> int:
""":class:`int`: Returns the red component of the colour."""
return self._get_byte(2)
@property
def g(self) -> int:
""":class:`int`: Returns the green component of the colour."""
return self._get_byte(1)
@property
def b(self) -> int:
""":class:`int`: Returns the blue component of the colour."""
return self._get_byte(0)
def to_rgb(self) -> Tuple[int, int, int]:
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
return (self.r, self.g, self.b)
@classmethod
def from_rgb(cls, r: int, g: int, b: int) -> Self:
"""Constructs a :class:`Colour` from an RGB tuple."""
return cls((r << 16) + (g << 8) + b)
@classmethod
def from_hsv(cls, h: float, s: float, v: float) -> Self:
"""Constructs a :class:`Colour` from an HSV tuple."""
rgb = colorsys.hsv_to_rgb(h, s, v)
return cls.from_rgb(*(int(x * 255) for x in rgb))
@classmethod
def default(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
return cls(0)
@classmethod
def random(cls, *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> Self:
"""A factory method that returns a :class:`Colour` with a random hue.
.. note::
The random algorithm works by choosing a colour with a random hue but
with maxed out saturation and value.
.. versionadded:: 1.6
Parameters
----------
seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]]
The seed to initialize the RNG with. If ``None`` is passed the default RNG is used.
.. versionadded:: 1.7
"""
rand = random if seed is None else random.Random(seed)
return cls.from_hsv(rand.random(), 1, 1)
@classmethod
def teal(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
return cls(0x1ABC9C)
@classmethod
def dark_teal(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
return cls(0x11806A)
@classmethod
def brand_green(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x57F287``.
.. versionadded:: 2.0
"""
return cls(0x57F287)
@classmethod
def green(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
return cls(0x2ECC71)
@classmethod
def dark_green(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
return cls(0x1F8B4C)
@classmethod
def blue(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
return cls(0x3498DB)
@classmethod
def dark_blue(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
return cls(0x206694)
@classmethod
def purple(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
return cls(0x9B59B6)
@classmethod
def dark_purple(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
return cls(0x71368A)
@classmethod
def magenta(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
return cls(0xE91E63)
@classmethod
def dark_magenta(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
return cls(0xAD1457)
@classmethod
def gold(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
return cls(0xF1C40F)
@classmethod
def dark_gold(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
return cls(0xC27C0E)
@classmethod
def orange(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
return cls(0xE67E22)
@classmethod
def dark_orange(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
return cls(0xA84300)
@classmethod
def brand_red(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xED4245``.
.. versionadded:: 2.0
"""
return cls(0xED4245)
@classmethod
def red(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
return cls(0xE74C3C)
@classmethod
def dark_red(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
return cls(0x992D22)
@classmethod
def lighter_grey(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
return cls(0x95A5A6)
lighter_gray = lighter_grey
@classmethod
def dark_grey(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
return cls(0x607D8B)
dark_gray = dark_grey
@classmethod
def light_grey(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
return cls(0x979C9F)
light_gray = light_grey
@classmethod
def darker_grey(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
return cls(0x546E7A)
darker_gray = darker_grey
@classmethod
def og_blurple(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
return cls(0x7289DA)
old_blurple = og_blurple
@classmethod
def blurple(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x5865F2``."""
return cls(0x5865F2)
@classmethod
def greyple(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
return cls(0x99AAB5)
@classmethod
def dark_theme(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x313338``.
This will appear transparent on Discord's dark theme.
.. versionadded:: 1.5
"""
return cls(0x313338)
@classmethod
def fuchsia(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459E``.
.. versionadded:: 2.0
"""
return cls(0xEB459E)
@classmethod
def yellow(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``.
.. versionadded:: 2.0
"""
return cls(0xFEE75C)
@classmethod
def light_embed(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xF2F3F5``.
This matches the embed background colour on Discord's light theme.
.. versionadded:: 2.10
"""
return cls(0xF2F3F5)
@classmethod
def dark_embed(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0x2B2D31``.
This matches the embed background colour on Discord's dark theme.
.. versionadded:: 2.10
"""
return cls(0x2B2D31)
@classmethod
def holographic_style(cls) -> Tuple[Self, Self, Self]:
"""A factory method that returns a tuple of :class:`Colour` with values of
``0xA9C9FF``, ``0xFFBBEC``, ``0xFFC3A0``. This matches the holographic colour style
for roles.
The first value represents the ``colour`` (``primary_color``), the second and the third
represents the ``secondary_colour`` and ``tertiary_colour`` respectively.
.. versionadded:: 2.11
"""
return cls(0xA9C9FF), cls(0xFFBBEC), cls(0xFFC3A0)
Color = Colour

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Optional, Type, Union
if TYPE_CHECKING:
from types import TracebackType
from typing_extensions import Self
from .abc import Messageable
from .channel import ThreadOnlyGuildChannel
__all__ = ("Typing",)
def _typing_done_callback(fut: asyncio.Future) -> None:
# just retrieve any exception and call it a day
try:
fut.exception()
except (asyncio.CancelledError, Exception):
pass
class Typing:
def __init__(self, messageable: Union[Messageable, ThreadOnlyGuildChannel]) -> None:
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
self.messageable: Union[Messageable, ThreadOnlyGuildChannel] = messageable
async def do_typing(self) -> None:
try:
channel = self._channel
except AttributeError:
channel = await self.messageable._get_channel()
typing = channel._state.http.send_typing
while True:
await typing(channel.id)
await asyncio.sleep(5)
def __enter__(self) -> Self:
self.task: asyncio.Task = self.loop.create_task(self.do_typing())
self.task.add_done_callback(_typing_done_callback)
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.task.cancel()
async def __aenter__(self) -> Self:
self._channel = channel = await self.messageable._get_channel()
await channel._state.http.send_typing(channel.id)
return self.__enter__()
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.task.cancel()

View File

@@ -0,0 +1,46 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
__all__ = (
"DiscordWarning",
"ConfigWarning",
"SyncWarning",
"LocalizationWarning",
)
class DiscordWarning(Warning):
"""Base warning class for disnake.
.. versionadded:: 2.3
"""
pass
class ConfigWarning(DiscordWarning):
"""Warning class related to configuration issues.
.. versionadded:: 2.3
"""
pass
class SyncWarning(DiscordWarning):
"""Warning class for application command synchronization issues.
.. versionadded:: 2.3
"""
pass
class LocalizationWarning(DiscordWarning):
"""Warning class for localization issues.
.. versionadded:: 2.5
"""
pass

View File

@@ -0,0 +1,952 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import datetime
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
List,
Literal,
Mapping,
Optional,
Protocol,
Sized,
Union,
cast,
overload,
)
from . import utils
from .colour import Colour
from .file import File
from .utils import MISSING, classproperty, warn_deprecated
__all__ = ("Embed",)
# backwards compatibility, hidden from type-checkers to have them show errors when accessed
if not TYPE_CHECKING:
def __getattr__(name: str) -> None:
if name == "EmptyEmbed":
warn_deprecated(
"`EmptyEmbed` is deprecated and will be removed in a future version. Use `None` instead.",
stacklevel=2,
)
return None
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class EmbedProxy:
def __init__(self, layer: Optional[Mapping[str, Any]]) -> None:
if layer is not None:
self.__dict__.update(layer)
def __len__(self) -> int:
return len(self.__dict__)
def __repr__(self) -> str:
inner = ", ".join((f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_")))
return f"EmbedProxy({inner})"
def __getattr__(self, attr: str) -> None:
return None
def __eq__(self, other: Any) -> bool:
return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__
if TYPE_CHECKING:
from typing_extensions import Self
from disnake.types.embed import (
Embed as EmbedData,
EmbedAuthor as EmbedAuthorPayload,
EmbedField as EmbedFieldPayload,
EmbedFooter as EmbedFooterPayload,
EmbedImage as EmbedImagePayload,
EmbedProvider as EmbedProviderPayload,
EmbedThumbnail as EmbedThumbnailPayload,
EmbedType,
EmbedVideo as EmbedVideoPayload,
)
class _EmbedFooterProxy(Sized, Protocol):
text: Optional[str]
icon_url: Optional[str]
proxy_icon_url: Optional[str]
class _EmbedFieldProxy(Sized, Protocol):
name: Optional[str]
value: Optional[str]
inline: Optional[bool]
class _EmbedMediaProxy(Sized, Protocol):
url: Optional[str]
proxy_url: Optional[str]
height: Optional[int]
width: Optional[int]
class _EmbedVideoProxy(Sized, Protocol):
url: Optional[str]
proxy_url: Optional[str]
height: Optional[int]
width: Optional[int]
class _EmbedProviderProxy(Sized, Protocol):
name: Optional[str]
url: Optional[str]
class _EmbedAuthorProxy(Sized, Protocol):
name: Optional[str]
url: Optional[str]
icon_url: Optional[str]
proxy_icon_url: Optional[str]
_FileKey = Literal["image", "thumbnail", "footer", "author"]
class Embed:
"""Represents a Discord embed.
.. collapse:: operations
.. describe:: x == y
Checks if two embeds are equal.
.. versionadded:: 2.6
.. describe:: x != y
Checks if two embeds are not equal.
.. versionadded:: 2.6
.. describe:: len(x)
Returns the total size of the embed.
Useful for checking if it's within the 6000 character limit.
Check if all aspects of the embed are within the limits with :func:`Embed.check_limits`.
.. describe:: bool(b)
Returns whether the embed has any data set.
.. versionadded:: 2.0
Certain properties return an ``EmbedProxy``, a type
that acts similar to a regular :class:`dict` except using dotted access,
e.g. ``embed.author.icon_url``.
For ease of use, all parameters that expect a :class:`str` are implicitly
cast to :class:`str` for you.
Attributes
----------
title: Optional[:class:`str`]
The title of the embed.
type: Optional[:class:`str`]
The type of embed. Usually "rich".
Possible strings for embed types can be found on Discord's
:ddocs:`api-docs <resources/channel#embed-object-embed-types>`.
description: Optional[:class:`str`]
The description of the embed.
url: Optional[:class:`str`]
The URL of the embed.
timestamp: Optional[:class:`datetime.datetime`]
The timestamp of the embed content. This is an aware datetime.
If a naive datetime is passed, it is converted to an aware
datetime with the local timezone.
colour: Optional[:class:`Colour`]
The colour code of the embed. Aliased to ``color`` as well.
In addition to :class:`Colour`, :class:`int` can also be assigned to it,
in which case the value will be converted to a :class:`Colour` object.
"""
__slots__ = (
"title",
"url",
"type",
"_timestamp",
"_colour",
"_footer",
"_image",
"_thumbnail",
"_video",
"_provider",
"_author",
"_fields",
"description",
"_files",
)
_default_colour: ClassVar[Optional[Colour]] = None
_colour: Optional[Colour]
def __init__(
self,
*,
title: Optional[Any] = None,
type: Optional[EmbedType] = "rich",
description: Optional[Any] = None,
url: Optional[Any] = None,
timestamp: Optional[datetime.datetime] = None,
colour: Optional[Union[int, Colour]] = MISSING,
color: Optional[Union[int, Colour]] = MISSING,
) -> None:
self.title: Optional[str] = str(title) if title is not None else None
self.type: Optional[EmbedType] = type
self.description: Optional[str] = str(description) if description is not None else None
self.url: Optional[str] = str(url) if url is not None else None
self.timestamp = timestamp
# possible values:
# - MISSING: embed color will be _default_color
# - None: embed color will not be set
# - Color: embed color will be set to specified color
if colour is not MISSING:
color = colour
self.colour = color
self._thumbnail: Optional[EmbedThumbnailPayload] = None
self._video: Optional[EmbedVideoPayload] = None
self._provider: Optional[EmbedProviderPayload] = None
self._author: Optional[EmbedAuthorPayload] = None
self._image: Optional[EmbedImagePayload] = None
self._footer: Optional[EmbedFooterPayload] = None
self._fields: Optional[List[EmbedFieldPayload]] = None
self._files: Dict[_FileKey, File] = {}
# see `EmptyEmbed` above
if not TYPE_CHECKING:
@classproperty
def Empty(self) -> None:
warn_deprecated(
"`Embed.Empty` is deprecated and will be removed in a future version. Use `None` instead.",
stacklevel=3,
)
return None
@classmethod
def from_dict(cls, data: EmbedData) -> Self:
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
format that Discord expects it to be in.
You can find out about this format in the
:ddocs:`official Discord documentation <resources/channel#embed-object>`.
Parameters
----------
data: :class:`dict`
The dictionary to convert into an embed.
"""
# we are bypassing __init__ here since it doesn't apply here
self = cls.__new__(cls)
# fill in the basic fields
self.title = str(title) if (title := data.get("title")) is not None else None
self.type = data.get("type")
self.description = (
str(description) if (description := data.get("description")) is not None else None
)
self.url = str(url) if (url := data.get("url")) is not None else None
self._files = {}
# try to fill in the more rich fields
self.colour = data.get("color")
self.timestamp = utils.parse_time(data.get("timestamp"))
self._thumbnail = data.get("thumbnail")
self._video = data.get("video")
self._provider = data.get("provider")
self._author = data.get("author")
self._image = data.get("image")
self._footer = data.get("footer")
self._fields = data.get("fields")
return self
def copy(self) -> Self:
"""Returns a shallow copy of the embed."""
embed = type(self).from_dict(self.to_dict())
# assign manually to keep behavior of default colors
embed._colour = self._colour
# copy files and fields collections
embed._files = self._files.copy()
if self._fields is not None:
embed._fields = self._fields.copy()
return embed
def __len__(self) -> int:
total = len((self.title or "").strip()) + len((self.description or "").strip())
if self._fields:
for field in self._fields:
total += len(field["name"].strip()) + len(field["value"].strip())
if self._footer and (footer_text := self._footer.get("text")):
total += len(footer_text.strip())
if self._author and (author_name := self._author.get("name")):
total += len(author_name.strip())
return total
def __bool__(self) -> bool:
return any(
(
self.title,
self.url,
self.description,
self._colour,
self._fields,
self._timestamp,
self._author,
self._thumbnail,
self._footer,
self._image,
self._provider,
self._video,
)
)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Embed):
return False
for slot in self.__slots__:
if slot == "_colour":
slot = "color"
if (getattr(self, slot) or None) != (getattr(other, slot) or None):
return False
return True
@property
def colour(self) -> Optional[Colour]:
col = self._colour
return col if col is not MISSING else type(self)._default_colour
@colour.setter
def colour(self, value: Optional[Union[int, Colour]]) -> None:
if isinstance(value, int):
self._colour = Colour(value=value)
elif value is MISSING or value is None or isinstance(value, Colour):
self._colour = value
else:
raise TypeError(
f"Expected disnake.Colour, int, or None but received {type(value).__name__} instead."
)
@colour.deleter
def colour(self) -> None:
self._colour = MISSING
color = colour
@property
def timestamp(self) -> Optional[datetime.datetime]:
return self._timestamp
@timestamp.setter
def timestamp(self, value: Optional[datetime.datetime]) -> None:
if isinstance(value, datetime.datetime):
if value.tzinfo is None:
value = value.astimezone()
self._timestamp = value
elif value is None:
self._timestamp = value
else:
raise TypeError(
f"Expected datetime.datetime or None received {type(value).__name__} instead"
)
@property
def footer(self) -> _EmbedFooterProxy:
"""Returns an ``EmbedProxy`` denoting the footer contents.
Possible attributes you can access are:
- ``text``
- ``icon_url``
- ``proxy_icon_url``
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedFooterProxy", EmbedProxy(self._footer))
@overload
def set_footer(self, *, text: Any, icon_url: Optional[Any] = ...) -> Self: ...
@overload
def set_footer(self, *, text: Any, icon_file: File = ...) -> Self: ...
def set_footer(
self, *, text: Any, icon_url: Optional[Any] = MISSING, icon_file: File = MISSING
) -> Self:
"""Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
At most one of ``icon_url`` or ``icon_file`` may be passed.
.. warning::
Passing a :class:`disnake.File` object will make the embed not
reusable.
.. warning::
If used with the other ``set_*`` methods, you must ensure
that the :attr:`.File.filename` is unique to avoid duplication.
Parameters
----------
text: :class:`str`
The footer text.
.. versionchanged:: 2.6
No longer optional, must be set to a valid string.
icon_url: Optional[:class:`str`]
The URL of the footer icon. Only HTTP(S) is supported.
icon_file: :class:`File`
The file to use as the footer icon.
.. versionadded:: 2.10
"""
self._footer = {
"text": str(text),
}
result = self._handle_resource(icon_url, icon_file, key="footer", required=False)
if result is not None:
self._footer["icon_url"] = result
return self
def remove_footer(self) -> Self:
"""Clears embed's footer information.
This function returns the class instance to allow for fluent-style
chaining.
.. versionadded:: 2.0
"""
self._footer = None
return self
@property
def image(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the image contents.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedMediaProxy", EmbedProxy(self._image))
@overload
def set_image(self, url: Optional[Any]) -> Self: ...
@overload
def set_image(self, *, file: File) -> Self: ...
def set_image(self, url: Optional[Any] = MISSING, *, file: File = MISSING) -> Self:
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Exactly one of ``url`` or ``file`` must be passed.
.. warning::
Passing a :class:`disnake.File` object will make the embed not
reusable.
.. warning::
If used with the other ``set_*`` methods, you must ensure
that the :attr:`.File.filename` is unique to avoid duplication.
.. versionchanged:: 1.4
Passing ``None`` removes the image.
Parameters
----------
url: Optional[:class:`str`]
The source URL for the image. Only HTTP(S) is supported.
file: :class:`File`
The file to use as the image.
.. versionadded:: 2.2
"""
result = self._handle_resource(url, file, key="image")
self._image = {"url": result} if result is not None else None
return self
@property
def thumbnail(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedMediaProxy", EmbedProxy(self._thumbnail))
@overload
def set_thumbnail(self, url: Optional[Any]) -> Self: ...
@overload
def set_thumbnail(self, *, file: File) -> Self: ...
def set_thumbnail(self, url: Optional[Any] = MISSING, *, file: File = MISSING) -> Self:
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Exactly one of ``url`` or ``file`` must be passed.
.. warning::
Passing a :class:`disnake.File` object will make the embed not
reusable.
.. warning::
If used with the other ``set_*`` methods, you must ensure
that the :attr:`.File.filename` is unique to avoid duplication.
.. versionchanged:: 1.4
Passing ``None`` removes the thumbnail.
Parameters
----------
url: Optional[:class:`str`]
The source URL for the thumbnail. Only HTTP(S) is supported.
file: :class:`File`
The file to use as the image.
.. versionadded:: 2.2
"""
result = self._handle_resource(url, file, key="thumbnail")
self._thumbnail = {"url": result} if result is not None else None
return self
@property
def video(self) -> _EmbedVideoProxy:
"""Returns an ``EmbedProxy`` denoting the video contents.
Possible attributes include:
- ``url`` for the video URL.
- ``proxy_url`` for the proxied video URL.
- ``height`` for the video height.
- ``width`` for the video width.
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedVideoProxy", EmbedProxy(self._video))
@property
def provider(self) -> _EmbedProviderProxy:
"""Returns an ``EmbedProxy`` denoting the provider contents.
The only attributes that might be accessed are ``name`` and ``url``.
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedProviderProxy", EmbedProxy(self._provider))
@property
def author(self) -> _EmbedAuthorProxy:
"""Returns an ``EmbedProxy`` denoting the author contents.
See :meth:`set_author` for possible values you can access.
If an attribute is not set, it will be ``None``.
"""
return cast("_EmbedAuthorProxy", EmbedProxy(self._author))
@overload
def set_author(
self, *, name: Any, url: Optional[Any] = ..., icon_url: Optional[Any] = ...
) -> Self: ...
@overload
def set_author(self, *, name: Any, url: Optional[Any] = ..., icon_file: File = ...) -> Self: ...
def set_author(
self,
*,
name: Any,
url: Optional[Any] = None,
icon_url: Optional[Any] = MISSING,
icon_file: File = MISSING,
) -> Self:
"""Sets the author for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
At most one of ``icon_url`` or ``icon_file`` may be passed.
.. warning::
Passing a :class:`disnake.File` object will make the embed not
reusable.
.. warning::
If used with the other ``set_*`` methods, you must ensure
that the :attr:`.File.filename` is unique to avoid duplication.
Parameters
----------
name: :class:`str`
The name of the author.
url: Optional[:class:`str`]
The URL for the author.
icon_url: Optional[:class:`str`]
The URL of the author icon. Only HTTP(S) is supported.
icon_file: :class:`File`
The file to use as the author icon.
.. versionadded:: 2.10
"""
self._author = {
"name": str(name),
}
if url is not None:
self._author["url"] = str(url)
result = self._handle_resource(icon_url, icon_file, key="author", required=False)
if result is not None:
self._author["icon_url"] = result
return self
def remove_author(self) -> Self:
"""Clears embed's author information.
This function returns the class instance to allow for fluent-style
chaining.
.. versionadded:: 1.4
"""
self._author = None
return self
@property
def fields(self) -> List[_EmbedFieldProxy]:
"""List[``EmbedProxy``]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
See :meth:`add_field` for possible values you can access.
If an attribute is not set, it will be ``None``.
"""
return cast("List[_EmbedFieldProxy]", [EmbedProxy(d) for d in (self._fields or [])])
def add_field(self, name: Any, value: Any, *, inline: bool = True) -> Self:
"""Adds a field to the embed object.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
name: :class:`str`
The name of the field.
value: :class:`str`
The value of the field.
inline: :class:`bool`
Whether the field should be displayed inline.
Defaults to ``True``.
"""
field: EmbedFieldPayload = {
"inline": inline,
"name": str(name),
"value": str(value),
}
if self._fields is not None:
self._fields.append(field)
else:
self._fields = [field]
return self
def insert_field_at(self, index: int, name: Any, value: Any, *, inline: bool = True) -> Self:
"""Inserts a field before a specified index to the embed.
This function returns the class instance to allow for fluent-style
chaining.
.. versionadded:: 1.2
Parameters
----------
index: :class:`int`
The index of where to insert the field.
name: :class:`str`
The name of the field.
value: :class:`str`
The value of the field.
inline: :class:`bool`
Whether the field should be displayed inline.
Defaults to ``True``.
"""
field: EmbedFieldPayload = {
"inline": inline,
"name": str(name),
"value": str(value),
}
if self._fields is not None:
self._fields.insert(index, field)
else:
self._fields = [field]
return self
def clear_fields(self) -> None:
"""Removes all fields from this embed."""
self._fields = None
def remove_field(self, index: int) -> None:
"""Removes a field at a specified index.
If the index is invalid or out of bounds then the error is
silently swallowed.
.. note::
When deleting a field by index, the index of the other fields
shift to fill the gap just like a regular list.
Parameters
----------
index: :class:`int`
The index of the field to remove.
"""
if self._fields is not None:
try:
del self._fields[index]
except IndexError:
pass
def set_field_at(self, index: int, name: Any, value: Any, *, inline: bool = True) -> Self:
"""Modifies a field to the embed object.
The index must point to a valid pre-existing field.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
index: :class:`int`
The index of the field to modify.
name: :class:`str`
The name of the field.
value: :class:`str`
The value of the field.
inline: :class:`bool`
Whether the field should be displayed inline.
Defaults to ``True``.
Raises
------
IndexError
An invalid index was provided.
"""
if not self._fields:
raise IndexError("field index out of range")
try:
self._fields[index]
except IndexError:
raise IndexError("field index out of range") from None
field: EmbedFieldPayload = {
"inline": inline,
"name": str(name),
"value": str(value),
}
self._fields[index] = field
return self
def to_dict(self) -> EmbedData:
"""Converts this embed object into a dict."""
# add in the raw data into the dict
result: EmbedData = {}
if self._footer is not None:
result["footer"] = self._footer
if self._image is not None:
result["image"] = self._image
if self._thumbnail is not None:
result["thumbnail"] = self._thumbnail
if self._video is not None:
result["video"] = self._video
if self._provider is not None:
result["provider"] = self._provider
if self._author is not None:
result["author"] = self._author
if self._fields is not None:
result["fields"] = self._fields
# deal with basic convenience wrappers
if self.colour:
result["color"] = self.colour.value
if self._timestamp:
result["timestamp"] = utils.isoformat_utc(self._timestamp)
# add in the non raw attribute ones
if self.type:
result["type"] = self.type
if self.description:
result["description"] = self.description
if self.url:
result["url"] = self.url
if self.title:
result["title"] = self.title
return result
@classmethod
def set_default_colour(cls, value: Optional[Union[int, Colour]]) -> Optional[Colour]:
"""Set the default colour of all new embeds.
.. versionadded:: 2.4
Returns
-------
Optional[:class:`Colour`]
The colour that was set.
"""
if value is None or isinstance(value, Colour):
cls._default_colour = value
elif isinstance(value, int):
cls._default_colour = Colour(value=value)
else:
raise TypeError(
f"Expected disnake.Colour, int, or None but received {type(value).__name__} instead."
)
return cls._default_colour
set_default_color = set_default_colour
@classmethod
def get_default_colour(cls) -> Optional[Colour]:
"""Get the default colour of all new embeds.
.. versionadded:: 2.4
Returns
-------
Optional[:class:`Colour`]
The default colour.
"""
return cls._default_colour
get_default_color = get_default_colour
def _handle_resource(
self, url: Optional[Any], file: Optional[File], *, key: _FileKey, required: bool = True
) -> Optional[str]:
if required:
if not (url is MISSING) ^ (file is MISSING):
raise TypeError("Exactly one of url or file must be provided")
else:
if url is not MISSING and file is not MISSING:
raise TypeError("At most one of url or file may be provided, not both.")
if file:
if file.filename is None:
raise TypeError("File must have a filename")
self._files[key] = file
return f"attachment://{file.filename}"
else:
self._files.pop(key, None)
return str(url) if url else None
def check_limits(self) -> None:
"""Checks if this embed fits within the limits dictated by Discord.
There is also a 6000 character limit across all embeds in a message.
Returns nothing on success, raises :exc:`ValueError` if an attribute exceeds the limits.
+--------------------------+------------------------------------+
| Field | Limit |
+--------------------------+------------------------------------+
| title | 256 characters |
+--------------------------+------------------------------------+
| description | 4096 characters |
+--------------------------+------------------------------------+
| fields | Up to 25 field objects |
+--------------------------+------------------------------------+
| field.name | 256 characters |
+--------------------------+------------------------------------+
| field.value | 1024 characters |
+--------------------------+------------------------------------+
| footer.text | 2048 characters |
+--------------------------+------------------------------------+
| author.name | 256 characters |
+--------------------------+------------------------------------+
.. versionadded:: 2.6
Raises
------
ValueError
One or more of the embed attributes are too long.
"""
if self.title and len(self.title.strip()) > 256:
raise ValueError("Embed title cannot be longer than 256 characters")
if self.description and len(self.description.strip()) > 4096:
raise ValueError("Embed description cannot be longer than 4096 characters")
if self._footer and len(self._footer.get("text", "").strip()) > 2048:
raise ValueError("Embed footer text cannot be longer than 2048 characters")
if self._author and len(self._author.get("name", "").strip()) > 256:
raise ValueError("Embed author name cannot be longer than 256 characters")
if self._fields:
if len(self._fields) > 25:
raise ValueError("Embeds cannot have more than 25 fields")
for field_index, field in enumerate(self._fields):
if len(field["name"].strip()) > 256:
raise ValueError(
f"Embed field {field_index} name cannot be longer than 256 characters"
)
if len(field["value"].strip()) > 1024:
raise ValueError(
f"Embed field {field_index} value cannot be longer than 1024 characters"
)
if len(self) > 6000:
raise ValueError("Embed total size cannot be longer than 6000 characters")

View File

@@ -0,0 +1,248 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union
from .asset import Asset, AssetMixin
from .partial_emoji import PartialEmoji, _EmojiTag
from .user import User
from .utils import MISSING, SnowflakeList, snowflake_time
__all__ = ("Emoji",)
if TYPE_CHECKING:
from datetime import datetime
from .abc import Snowflake
from .guild import Guild
from .guild_preview import GuildPreview
from .role import Role
from .state import ConnectionState
from .types.emoji import Emoji as EmojiPayload
class Emoji(_EmojiTag, AssetMixin):
"""Represents a custom emoji.
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
.. collapse:: operations
.. describe:: x == y
Checks if two emoji are the same.
.. describe:: x != y
Checks if two emoji are not the same.
.. describe:: hash(x)
Return the emoji's hash.
.. describe:: iter(x)
Returns an iterator of ``(field, value)`` pairs. This allows this class
to be used as an iterable in list/dict/etc constructions.
.. describe:: str(x)
Returns the emoji rendered for Discord.
Attributes
----------
name: :class:`str`
The emoji's name.
id: :class:`int`
The emoji's ID.
require_colons: :class:`bool`
Whether colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
animated: :class:`bool`
Whether the emoji is animated or not.
managed: :class:`bool`
Whether the emoji is managed by a Twitch integration.
guild_id: :class:`int`
The guild ID the emoji belongs to.
available: :class:`bool`
Whether the emoji is available for use.
user: Optional[:class:`User`]
The user that created this emoji. This can only be retrieved using
:meth:`Guild.fetch_emoji`/:meth:`Guild.fetch_emojis` while
having the :attr:`~Permissions.manage_guild_expressions` permission.
"""
__slots__: Tuple[str, ...] = (
"require_colons",
"animated",
"managed",
"id",
"name",
"_roles",
"guild_id",
"user",
"available",
)
def __init__(
self, *, guild: Union[Guild, GuildPreview], state: ConnectionState, data: EmojiPayload
) -> None:
self.guild_id: int = guild.id
self._state: ConnectionState = state
self._from_data(data)
def _from_data(self, emoji: EmojiPayload) -> None:
self.require_colons: bool = emoji.get("require_colons", False)
self.managed: bool = emoji.get("managed", False)
self.id: int = int(emoji["id"]) # type: ignore
self.name: str = emoji["name"] # type: ignore
self.animated: bool = emoji.get("animated", False)
self.available: bool = emoji.get("available", True)
self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get("roles", [])))
user = emoji.get("user")
self.user: Optional[User] = User(state=self._state, data=user) if user else None
def _to_partial(self) -> PartialEmoji:
return PartialEmoji(name=self.name, animated=self.animated, id=self.id)
def __iter__(self) -> Iterator[Tuple[str, Any]]:
for attr in self.__slots__:
if attr[0] != "_":
value = getattr(self, attr, None)
if value is not None:
yield (attr, value)
def __str__(self) -> str:
if self.animated:
return f"<a:{self.name}:{self.id}>"
return f"<:{self.name}:{self.id}>"
def __repr__(self) -> str:
return f"<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>"
def __eq__(self, other: Any) -> bool:
return isinstance(other, _EmojiTag) and self.id == other.id
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return self.id >> 22
@property
def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
return snowflake_time(self.id)
@property
def url(self) -> str:
""":class:`str`: Returns the URL of the emoji."""
fmt = "gif" if self.animated else "png"
return f"{Asset.BASE}/emojis/{self.id}.{fmt}"
@property
def roles(self) -> List[Role]:
"""List[:class:`Role`]: A :class:`list` of roles that are allowed to use this emoji.
If roles is empty, the emoji is unrestricted.
Emojis with :attr:`subscription roles <RoleTags.integration_id>` are considered premium emojis,
and count towards a separate limit of 25 emojis.
"""
guild = self.guild
if guild is None: # pyright: ignore[reportUnnecessaryComparison]
return []
return [role for role in guild.roles if self._roles.has(role.id)]
@property
def guild(self) -> Guild:
""":class:`Guild`: The guild this emoji belongs to."""
# this will most likely never return None but there's a possibility
return self._state._get_guild(self.guild_id) # type: ignore
def is_usable(self) -> bool:
"""Whether the bot can use this emoji.
.. versionadded:: 1.3
:return type: :class:`bool`
"""
if not self.available:
return False
if not self._roles:
return True
emoji_roles, my_roles = self._roles, self.guild.me._roles
return any(my_roles.has(role_id) for role_id in emoji_roles)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the custom emoji.
You must have :attr:`~Permissions.manage_guild_expressions` permission to
do this.
Parameters
----------
reason: Optional[:class:`str`]
The reason for deleting this emoji. Shows up on the audit log.
Raises
------
Forbidden
You are not allowed to delete this emoji.
HTTPException
An error occurred deleting the emoji.
"""
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
async def edit(
self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None
) -> Emoji:
"""|coro|
Edits the custom emoji.
You must have :attr:`~Permissions.manage_guild_expressions` permission to
do this.
.. versionchanged:: 2.0
The newly updated emoji is returned.
Parameters
----------
name: :class:`str`
The new emoji name.
roles: Optional[List[:class:`~disnake.abc.Snowflake`]]
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
An emoji cannot have both subscription roles (see :attr:`RoleTags.integration_id`) and
non-subscription roles, and emojis can't be converted between premium and non-premium
after creation.
reason: Optional[:class:`str`]
The reason for editing this emoji. Shows up on the audit log.
Raises
------
Forbidden
You are not allowed to edit this emoji.
HTTPException
An error occurred editing the emoji.
Returns
-------
:class:`Emoji`
The newly updated emoji.
"""
payload = {}
if name is not MISSING:
payload["name"] = name
if roles is not MISSING:
payload["roles"] = [role.id for role in roles]
data = await self._state.http.edit_custom_emoji(
self.guild.id, self.id, payload=payload, reason=reason
)
return Emoji(guild=self.guild, data=data, state=self._state)

View File

@@ -0,0 +1,193 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Optional
from .enums import EntitlementType, try_enum
from .mixins import Hashable
from .utils import _get_as_snowflake, parse_time, snowflake_time, utcnow
if TYPE_CHECKING:
from .guild import Guild
from .state import ConnectionState
from .types.entitlement import Entitlement as EntitlementPayload
from .user import User
__all__ = ("Entitlement",)
class Entitlement(Hashable):
"""Represents an entitlement.
This is usually retrieved using :meth:`Client.entitlements`, from
:attr:`Interaction.entitlements` when using interactions, or provided by
events (e.g. :func:`on_entitlement_create`).
Note that some entitlements may have ended already; consider using
:meth:`is_active` to check whether a given entitlement is considered active at the current time,
or use ``exclude_ended=True`` when fetching entitlements using :meth:`Client.entitlements`.
You may create new entitlements for testing purposes using :meth:`Client.create_entitlement`.
.. collapse:: operations
.. describe:: x == y
Checks if two :class:`Entitlement`\\s are equal.
.. describe:: x != y
Checks if two :class:`Entitlement`\\s are not equal.
.. describe:: hash(x)
Returns the entitlement's hash.
.. versionadded:: 2.10
Attributes
----------
id: :class:`int`
The entitlement's ID.
type: :class:`EntitlementType`
The entitlement's type.
sku_id: :class:`int`
The ID of the associated SKU.
user_id: Optional[:class:`int`]
The ID of the user that is granted access to the entitlement's SKU.
See also :attr:`user`.
guild_id: Optional[:class:`int`]
The ID of the guild that is granted access to the entitlement's SKU.
See also :attr:`guild`.
application_id: :class:`int`
The parent application's ID.
deleted: :class:`bool`
Whether the entitlement has been deleted.
consumed: :class:`bool`
Whether the entitlement has been consumed. Only applies to consumable items,
i.e. those associated with a :attr:`~SKUType.consumable` SKU.
starts_at: Optional[:class:`datetime.datetime`]
The time at which the entitlement starts being active.
Set to ``None`` when this is a test entitlement.
ends_at: Optional[:class:`datetime.datetime`]
The time at which the entitlement stops being active.
You can use :meth:`is_active` to check whether this entitlement is still active.
"""
__slots__ = (
"_state",
"id",
"sku_id",
"user_id",
"guild_id",
"application_id",
"type",
"deleted",
"consumed",
"starts_at",
"ends_at",
)
def __init__(self, *, data: EntitlementPayload, state: ConnectionState) -> None:
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.sku_id: int = int(data["sku_id"])
self.user_id: Optional[int] = _get_as_snowflake(data, "user_id")
self.guild_id: Optional[int] = _get_as_snowflake(data, "guild_id")
self.application_id: int = int(data["application_id"])
self.type: EntitlementType = try_enum(EntitlementType, data["type"])
self.deleted: bool = data.get("deleted", False)
self.consumed: bool = data.get("consumed", False)
self.starts_at: Optional[datetime.datetime] = parse_time(data.get("starts_at"))
self.ends_at: Optional[datetime.datetime] = parse_time(data.get("ends_at"))
def __repr__(self) -> str:
# presumably one of these is set
if self.user_id:
grant_repr = f"user_id={self.user_id!r}"
else:
grant_repr = f"guild_id={self.guild_id!r}"
return (
f"<Entitlement id={self.id!r} sku_id={self.sku_id!r} type={self.type!r} {grant_repr}>"
)
@property
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the entitlement's creation time in UTC."""
return snowflake_time(self.id)
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild that is granted access to
this entitlement's SKU, if applicable.
"""
return self._state._get_guild(self.guild_id)
@property
def user(self) -> Optional[User]:
"""Optional[:class:`User`]: The user that is granted access to
this entitlement's SKU, if applicable.
Requires the user to be cached.
See also :attr:`user_id`.
"""
return self._state.get_user(self.user_id)
def is_active(self) -> bool:
"""Whether the entitlement is currently active,
based on :attr:`starts_at` and :attr:`ends_at`.
Always returns ``True`` for test entitlements.
:return type: :class:`bool`
"""
if self.deleted:
return False
now = utcnow()
if self.starts_at is not None and now < self.starts_at:
return False
if self.ends_at is not None and now >= self.ends_at:
return False
return True
async def consume(self) -> None:
"""|coro|
Marks the entitlement as consumed.
This is only valid for consumable one-time entitlements; see :attr:`consumed`.
Raises
------
NotFound
The entitlement does not exist.
HTTPException
Consuming the entitlement failed.
"""
await self._state.http.consume_entitlement(self.application_id, self.id)
async def delete(self) -> None:
"""|coro|
Deletes the entitlement.
This is only valid for test entitlements; you cannot use this to
delete entitlements that users purchased.
Raises
------
NotFound
The entitlement does not exist.
HTTPException
Deleting the entitlement failed.
"""
await self._state.http.delete_test_entitlement(self.application_id, self.id)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,435 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Union
if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientWebSocketResponse
from requests import Response
from .client import SessionStartLimit
from .interactions import Interaction, ModalInteraction
_ResponseType = Union[ClientResponse, Response]
__all__ = (
"DiscordException",
"ClientException",
"NoMoreItems",
"GatewayNotFound",
"HTTPException",
"Forbidden",
"NotFound",
"DiscordServerError",
"InvalidData",
"WebhookTokenMissing",
"LoginFailure",
"SessionStartLimitReached",
"ConnectionClosed",
"PrivilegedIntentsRequired",
"InteractionException",
"InteractionTimedOut",
"InteractionResponded",
"InteractionNotResponded",
"ModalChainNotSupported",
"InteractionNotEditable",
"LocalizationKeyError",
)
class DiscordException(Exception):
"""Base exception class for disnake.
Ideally speaking, this could be caught to handle any exceptions raised from this library.
"""
pass
class ClientException(DiscordException):
"""Exception that's raised when an operation in the :class:`Client` fails.
These are usually for exceptions that happened due to user input.
"""
pass
class NoMoreItems(DiscordException):
"""Exception that is raised when an async iteration operation has no more items."""
pass
class GatewayNotFound(DiscordException):
"""An exception that is raised when the gateway for Discord could not be found"""
def __init__(self) -> None:
message = "The gateway to connect to Discord was not found."
super().__init__(message)
def _flatten_error_dict(d: Dict[str, Any], key: str = "") -> Dict[str, str]:
items: List[Tuple[str, str]] = []
for k, v in d.items():
new_key = f"{key}.{k}" if key else k
if isinstance(v, dict):
try:
_errors: List[Dict[str, Any]] = v["_errors"]
except KeyError:
items.extend(_flatten_error_dict(v, new_key).items())
else:
items.append((new_key, " ".join(x.get("message", "") for x in _errors)))
else:
items.append((new_key, v))
return dict(items)
class HTTPException(DiscordException):
"""Exception that's raised when an HTTP request operation fails.
Attributes
----------
response: :class:`aiohttp.ClientResponse`
The response of the failed HTTP request. This is an
instance of :class:`aiohttp.ClientResponse`. In some cases
this could also be a :class:`requests.Response`.
text: :class:`str`
The text of the error. Could be an empty string.
status: :class:`int`
The status code of the HTTP request.
code: :class:`int`
The Discord specific error code for the failure.
"""
def __init__(
self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]
) -> None:
self.response: _ResponseType = response
self.status: int = response.status # type: ignore
self.code: int
self.text: str
if isinstance(message, dict):
self.code = message.get("code", 0)
base = message.get("message", "")
errors = message.get("errors")
if errors:
errors = _flatten_error_dict(errors)
helpful = "\n".join(f"In {k}: {m}" for k, m in errors.items())
self.text = base + "\n" + helpful
else:
self.text = base
else:
self.text = message or ""
self.code = 0
fmt = "{0.status} {0.reason} (error code: {1})"
if len(self.text):
fmt += ": {2}"
super().__init__(fmt.format(self.response, self.code, self.text))
class Forbidden(HTTPException):
"""Exception that's raised for when status code 403 occurs.
Subclass of :exc:`HTTPException`.
"""
pass
class NotFound(HTTPException):
"""Exception that's raised for when status code 404 occurs.
Subclass of :exc:`HTTPException`.
"""
pass
class DiscordServerError(HTTPException):
"""Exception that's raised for when a 500 range status code occurs.
Subclass of :exc:`HTTPException`.
.. versionadded:: 1.5
"""
pass
class InvalidData(ClientException):
"""Exception that's raised when the library encounters unknown
or invalid data from Discord.
"""
pass
class WebhookTokenMissing(DiscordException):
"""Exception that's raised when a :class:`Webhook` or :class:`SyncWebhook` is missing a token to make requests with.
.. versionadded:: 2.6
"""
pass
class LoginFailure(ClientException):
"""Exception that's raised when the :meth:`Client.login` function
fails to log you in from improper credentials or some other misc.
failure.
"""
pass
class SessionStartLimitReached(ClientException):
"""Exception that's raised when :meth:`Client.connect` function
fails to connect to Discord due to the session start limit being reached.
.. versionadded:: 2.6
Attributes
----------
session_start_limit: :class:`.SessionStartLimit`
The current state of the session start limit.
"""
def __init__(self, session_start_limit: SessionStartLimit, requested: int = 1) -> None:
self.session_start_limit: SessionStartLimit = session_start_limit
super().__init__(
f"Daily session start limit has been reached, resets at {self.session_start_limit.reset_time} "
f"Requested {requested} shards, have only {session_start_limit.remaining} remaining."
)
class ConnectionClosed(ClientException):
"""Exception that's raised when the gateway connection is
closed for reasons that could not be handled internally.
Attributes
----------
code: :class:`int`
The close code of the websocket.
reason: :class:`str`
The reason provided for the closure.
shard_id: Optional[:class:`int`]
The shard ID that got closed if applicable.
"""
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
GATEWAY_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = {
4000: "Unknown error",
4001: "Unknown opcode",
4002: "Decode error",
4003: "Not authenticated",
4004: "Authentication failed",
4005: "Already authenticated",
4007: "Invalid sequence",
4008: "Rate limited",
4009: "Session timed out",
4010: "Invalid Shard",
4011: "Sharding required - you are required to shard your connection in order to connect.",
4012: "Invalid API version",
4013: "Invalid intents",
4014: "Disallowed intents - you tried to specify an intent that you have not enabled or are not approved for.",
}
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes
GATEWAY_VOICE_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = {
**GATEWAY_CLOSE_EVENT_REASONS,
4002: "Failed to decode payload",
4006: "Session no longer valid",
4011: "Server not found",
4012: "Unknown protocol",
4014: "Disconnected (you were kicked, the main gateway session was dropped, etc.)",
4015: "Voice server crashed",
4016: "Unknown encryption mode",
4020: "Bad request - you sent a malformed request",
4021: "Disconnected: Rate Limited",
4022: "Disconnected: Call Terminated (channel deleted, voice server changed, etc.)",
}
def __init__(
self,
socket: ClientWebSocketResponse,
*,
shard_id: Optional[int],
code: Optional[int] = None,
voice: bool = False,
) -> None:
# This exception is just the same exception except
# reconfigured to subclass ClientException for users
self.code: int = code or socket.close_code or -1
# aiohttp doesn't seem to consistently provide close reason
self.reason: str = self.GATEWAY_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason")
if voice:
self.reason = self.GATEWAY_VOICE_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason")
self.shard_id: Optional[int] = shard_id
super().__init__(
f"Shard ID {self.shard_id} WebSocket closed with {self.code}: {self.reason}"
)
class PrivilegedIntentsRequired(ClientException):
"""Exception that's raised when the gateway is requesting privileged intents
but they're not ticked in the developer page yet.
Go to https://discord.com/developers/applications/ and enable the intents
that are required. Currently these are as follows:
- :attr:`Intents.members`
- :attr:`Intents.presences`
- :attr:`Intents.message_content`
Attributes
----------
shard_id: Optional[:class:`int`]
The shard ID that got closed if applicable.
"""
def __init__(self, shard_id: Optional[int]) -> None:
self.shard_id: Optional[int] = shard_id
msg = (
f"Shard ID {shard_id} is requesting privileged intents that have not been explicitly enabled in the "
"developer portal. It is recommended to go to https://discord.com/developers/applications/ "
"and explicitly enable the privileged intents within your application's page. If this is not "
"possible, then consider disabling the privileged intents instead."
)
super().__init__(msg)
class InteractionException(ClientException):
"""Exception that's raised when an interaction operation fails
.. versionadded:: 2.0
Attributes
----------
interaction: :class:`Interaction`
The interaction that was responded to.
"""
interaction: Interaction
class InteractionTimedOut(InteractionException):
"""Exception that's raised when an interaction takes more than 3 seconds
to respond but is not deferred.
.. versionadded:: 2.0
Attributes
----------
interaction: :class:`Interaction`
The interaction that was responded to.
"""
def __init__(self, interaction: Interaction) -> None:
self.interaction: Interaction = interaction
msg = (
"Interaction took more than 3 seconds to be responded to. "
'Please defer it using "interaction.response.defer" on the start of your command. '
"Later you may send a response by editing the deferred message "
'using "interaction.edit_original_response"'
"\n"
"Note: This might also be caused by a misconfiguration in the components "
"make sure you do not respond twice in case this is a component."
)
super().__init__(msg)
class InteractionResponded(InteractionException):
"""Exception that's raised when sending another interaction response using
:class:`InteractionResponse` when one has already been done before.
An interaction can only be responded to once.
.. versionadded:: 2.0
Attributes
----------
interaction: :class:`Interaction`
The interaction that's already been responded to.
"""
def __init__(self, interaction: Interaction) -> None:
self.interaction: Interaction = interaction
super().__init__("This interaction has already been responded to before")
class InteractionNotResponded(InteractionException):
"""Exception that's raised when editing an interaction response without
sending a response message first.
An interaction must be responded to exactly once.
.. versionadded:: 2.0
Attributes
----------
interaction: :class:`Interaction`
The interaction that hasn't been responded to.
"""
def __init__(self, interaction: Interaction) -> None:
self.interaction: Interaction = interaction
super().__init__("This interaction hasn't been responded to yet")
class ModalChainNotSupported(InteractionException):
"""Exception that's raised when responding to a modal with another modal.
.. versionadded:: 2.4
Attributes
----------
interaction: :class:`ModalInteraction`
The interaction that was responded to.
"""
def __init__(self, interaction: ModalInteraction) -> None:
self.interaction: ModalInteraction = interaction
super().__init__("You cannot respond to a modal with another modal.")
class InteractionNotEditable(InteractionException):
"""Exception that's raised when trying to use :func:`InteractionResponse.edit_message`
on an interaction without an associated message (which is thus non-editable).
.. versionadded:: 2.5
Attributes
----------
interaction: :class:`Interaction`
The interaction that was responded to.
"""
def __init__(self, interaction: Interaction) -> None:
self.interaction: Interaction = interaction
super().__init__("This interaction does not have a message to edit.")
class LocalizationKeyError(DiscordException):
"""Exception that's raised when a localization key lookup fails.
.. versionadded:: 2.5
Attributes
----------
key: :class:`str`
The localization key that couldn't be found.
"""
def __init__(self, key: str) -> None:
self.key: str = key
super().__init__(f"No localizations were found for the key '{key}'.")

View File

@@ -0,0 +1,26 @@
# SPDX-License-Identifier: MIT
"""disnake.ext.commands
~~~~~~~~~~~~~~~~~~~~~
An extension module to facilitate creation of bot commands.
:copyright: (c) 2015-2021 Rapptz, 2021-present Disnake Development
:license: MIT, see LICENSE for more details.
"""
from .base_core import *
from .bot import *
from .cog import *
from .context import *
from .converter import *
from .cooldowns import *
from .core import *
from .ctx_menus_core import *
from .custom_warnings import *
from .errors import *
from .flag_converter import *
from .flags import *
from .help import *
from .params import *
from .slash_core import *

Some files were not shown because too many files have changed in this diff Show More