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