On branch DiscordProfile
Initial commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Discord API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A basic wrapper for the Discord API.
|
||||
|
||||
:copyright: (c) 2015-2021 Rapptz & (c) 2021-present Pycord Development
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__title__ = "pycord"
|
||||
__author__ = "Pycord Development"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2015-2021 Rapptz & Copyright 2021-present Pycord Development"
|
||||
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# We need __version__ to be imported first
|
||||
# isort: off
|
||||
from ._version import *
|
||||
|
||||
# isort: on
|
||||
|
||||
|
||||
from . import abc, opus, sinks, ui, utils
|
||||
from .activity import *
|
||||
from .appinfo import *
|
||||
from .application_role_connection import *
|
||||
from .asset import *
|
||||
from .audit_logs import *
|
||||
from .automod import *
|
||||
from .bot import *
|
||||
from .channel import *
|
||||
from .client import *
|
||||
from .cog import *
|
||||
from .collectibles import *
|
||||
from .colour import *
|
||||
from .commands import *
|
||||
from .components import *
|
||||
from .embeds import *
|
||||
from .emoji import *
|
||||
from .enums import *
|
||||
from .errors import *
|
||||
from .file import *
|
||||
from .flags import *
|
||||
from .guild import *
|
||||
from .http import *
|
||||
from .incidents import *
|
||||
from .integrations import *
|
||||
from .interactions import *
|
||||
from .invite import *
|
||||
from .member import *
|
||||
from .mentions import *
|
||||
from .message import *
|
||||
from .monetization import *
|
||||
from .object import *
|
||||
from .onboarding import *
|
||||
from .partial_emoji import *
|
||||
from .permissions import *
|
||||
from .player import *
|
||||
from .poll import *
|
||||
from .primary_guild import *
|
||||
from .raw_models import *
|
||||
from .reaction import *
|
||||
from .role import *
|
||||
from .scheduled_events import *
|
||||
from .shard import *
|
||||
from .soundboard import *
|
||||
from .stage_instance import *
|
||||
from .sticker import *
|
||||
from .team import *
|
||||
from .template import *
|
||||
from .threads import *
|
||||
from .user import *
|
||||
from .webhook import *
|
||||
from .welcome_screen import *
|
||||
from .widget import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from discord.voice import VoiceClient as VoiceClientC
|
||||
from discord.voice import VoiceProtocol as VoiceProtocolC
|
||||
|
||||
C = TypeVar("C", bound=Client)
|
||||
|
||||
@deprecated(
|
||||
"discord.VoiceClient is deprecated in favour of discord.voice.VoiceClient since 2.7 and will be removed in 3.0",
|
||||
)
|
||||
class VoiceClient(VoiceClientC): ...
|
||||
|
||||
@deprecated(
|
||||
"discord.VoiceProtocol is deprecated in favour of discord.voice.VoiceProtocol since 2.7 and will be removed in 3.0",
|
||||
)
|
||||
class VoiceProtocol(VoiceProtocolC[C], Generic[C]): ...
|
||||
|
||||
else:
|
||||
from .utils import warn_deprecated
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "VoiceClient":
|
||||
warn_deprecated(
|
||||
"discord.VoiceClient", "discord.voice.VoiceClient", "2.7", "3.0"
|
||||
)
|
||||
from .voice import VoiceClient
|
||||
|
||||
return VoiceClient
|
||||
if name == "VoiceProtocol":
|
||||
warn_deprecated(
|
||||
"discord.VoiceProtocol", "discord.voice.VoiceProtocol", "2.7", "3.0"
|
||||
)
|
||||
from .voice import VoiceProtocol
|
||||
|
||||
return VoiceProtocol
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.metadata
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
def show_version() -> None:
|
||||
entries = [
|
||||
"- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(
|
||||
sys.version_info
|
||||
)
|
||||
]
|
||||
|
||||
version_info = discord.version_info
|
||||
entries.append(
|
||||
"- py-cord v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info)
|
||||
)
|
||||
if version_info.releaselevel != "final":
|
||||
version = importlib.metadata.version("py-cord")
|
||||
if version:
|
||||
entries.append(f" - py-cord importlib.metadata: v{version}")
|
||||
|
||||
entries.append(f"- aiohttp v{aiohttp.__version__}")
|
||||
uname = platform.uname()
|
||||
entries.append("- system info: {0.system} {0.release} {0.version}".format(uname))
|
||||
print("\n".join(entries))
|
||||
|
||||
|
||||
def core(parser, args) -> None:
|
||||
if args.version:
|
||||
show_version()
|
||||
|
||||
|
||||
_bot_template = """#!/usr/bin/env python3
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import config
|
||||
|
||||
class Bot(commands.{base}):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs)
|
||||
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
|
||||
|
||||
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 discord.ext import commands
|
||||
import discord
|
||||
|
||||
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))
|
||||
'''
|
||||
|
||||
_cog_extras = """
|
||||
def cog_unload(self):
|
||||
# clean up logic goes here
|
||||
pass
|
||||
|
||||
async def cog_check(self, ctx):
|
||||
# checks that apply to every command in here
|
||||
return True
|
||||
|
||||
async def bot_check(self, ctx):
|
||||
# checks that apply to every command to the bot
|
||||
return True
|
||||
|
||||
async def bot_check_once(self, ctx):
|
||||
# check that apply to every command but is guaranteed to be called only once
|
||||
return True
|
||||
|
||||
async def cog_command_error(self, ctx, error):
|
||||
# error handling to every command in here
|
||||
pass
|
||||
|
||||
async def cog_before_invoke(self, ctx):
|
||||
# called before a command is called here
|
||||
pass
|
||||
|
||||
async def cog_after_invoke(self, ctx):
|
||||
# called after a command is called here
|
||||
pass
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# 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
|
||||
_base_table = {
|
||||
"<": "-",
|
||||
">": "-",
|
||||
":": "-",
|
||||
'"': "-",
|
||||
# '/': '-', these are fine
|
||||
# '\\': '-',
|
||||
"|": "-",
|
||||
"?": "-",
|
||||
"*": "-",
|
||||
}
|
||||
|
||||
# NUL (0) and 1-31 are disallowed
|
||||
_base_table.update((chr(i), None) for i in range(32))
|
||||
|
||||
_translation_table = str.maketrans(_base_table)
|
||||
|
||||
|
||||
def to_path(parser, name, *, replace_spaces=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:
|
||||
base = "Bot" if not args.sharded else "AutoShardedBot"
|
||||
fp.write(_bot_template.format(base=base, prefix=args.prefix))
|
||||
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: argparse._SubParsersAction) -> 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(),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix", help="the bot prefix (default: $)", default="$", metavar="<prefix>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sharded", help="whether to use AutoShardedBot", 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: argparse._SubParsersAction) -> 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="discord", description="Tools for helping with Pycord"
|
||||
)
|
||||
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.
BIN
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.
BIN
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.
BIN
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.
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import warnings
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
from typing_extensions import TypedDict, deprecated
|
||||
|
||||
__all__ = ("__version__", "VersionInfo", "version_info")
|
||||
|
||||
from typing import Literal, NamedTuple
|
||||
|
||||
try:
|
||||
__version__ = version("py-cord")
|
||||
except PackageNotFoundError:
|
||||
# Package is not installed
|
||||
try:
|
||||
from setuptools_scm import get_version # type: ignore[import]
|
||||
|
||||
__version__ = get_version()
|
||||
except ImportError:
|
||||
# setuptools_scm is not installed
|
||||
__version__ = "0.0.0"
|
||||
warnings.warn(
|
||||
(
|
||||
"Package is not installed, and setuptools_scm is not installed. "
|
||||
f"As a fallback, {__name__}.__version__ will be set to {__version__}"
|
||||
),
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class AdvancedVersionInfo(TypedDict):
|
||||
serial: int
|
||||
build: int | None
|
||||
commit: str | None
|
||||
date: datetime.date | None
|
||||
|
||||
|
||||
class VersionInfo(NamedTuple):
|
||||
major: int
|
||||
minor: int
|
||||
micro: int
|
||||
releaselevel: Literal["alpha", "beta", "candidate", "final"]
|
||||
|
||||
# We can't set instance attributes on a NamedTuple, so we have to use a
|
||||
# global variable to store the advanced version info.
|
||||
@property
|
||||
def advanced(self) -> AdvancedVersionInfo:
|
||||
return _advanced
|
||||
|
||||
@advanced.setter
|
||||
def advanced(self, value: object) -> None:
|
||||
global _advanced
|
||||
_advanced = value
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
"VersionInfo.release_level is deprecated since version 2.4, consider using releaselevel instead."
|
||||
)
|
||||
def release_level(self) -> Literal["alpha", "beta", "candidate", "final"]:
|
||||
return self.releaselevel
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
'VersionInfo.serial is deprecated since version 2.4, consider using .advanced["serial"] instead.'
|
||||
)
|
||||
def serial(self) -> int:
|
||||
return self.advanced["serial"]
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
'VersionInfo.build is deprecated since version 2.4, consider using .advanced["build"] instead.'
|
||||
)
|
||||
def build(self) -> int | None:
|
||||
return self.advanced["build"]
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
'VersionInfo.commit is deprecated since version 2.4, consider using .advanced["commit"] instead.'
|
||||
)
|
||||
def commit(self) -> str | None:
|
||||
return self.advanced["commit"]
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
'VersionInfo.date is deprecated since version 2.4, consider using .advanced["date"] instead.'
|
||||
)
|
||||
def date(self) -> datetime.date | None:
|
||||
return self.advanced["date"]
|
||||
|
||||
|
||||
version_regex = re.compile(
|
||||
r"^(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>\d+))?"
|
||||
r"(?:(?P<level>rc|a|b)(?P<serial>\d+))?"
|
||||
r"(?:\.dev(?P<build>\d+))?"
|
||||
r"(?:\+(?:(?:g(?P<commit>[a-fA-F0-9]{4,40})(?:\.d(?P<date>\d{4}\d{2}\d{2})|))|d(?P<date1>\d{4}\d{2}\d{2})))?$"
|
||||
)
|
||||
version_match = version_regex.match(__version__)
|
||||
if version_match is None:
|
||||
raise RuntimeError(f"Invalid version string: {__version__}")
|
||||
raw_info = version_match.groupdict()
|
||||
|
||||
level_info: Literal["alpha", "beta", "candidate", "final"]
|
||||
|
||||
if raw_info["level"] == "a":
|
||||
level_info = "alpha"
|
||||
elif raw_info["level"] == "b":
|
||||
level_info = "beta"
|
||||
elif raw_info["level"] == "rc":
|
||||
level_info = "candidate"
|
||||
elif raw_info["level"] is None:
|
||||
level_info = "final"
|
||||
else:
|
||||
raise RuntimeError("Invalid release level")
|
||||
|
||||
if (raw_date := raw_info["date"] or raw_info["date1"]) is not None:
|
||||
date_info = datetime.date(
|
||||
int(raw_date[:4]),
|
||||
int(raw_date[4:6]),
|
||||
int(raw_date[6:]),
|
||||
)
|
||||
else:
|
||||
date_info = None
|
||||
|
||||
version_info: VersionInfo = VersionInfo(
|
||||
major=int(raw_info["major"] or 0) or None,
|
||||
minor=int(raw_info["minor"] or 0) or None,
|
||||
micro=int(raw_info["patch"] or 0) or None,
|
||||
releaselevel=level_info,
|
||||
)
|
||||
|
||||
_advanced = AdvancedVersionInfo(
|
||||
serial=raw_info["serial"],
|
||||
build=int(raw_info["build"] or 0) or None,
|
||||
commit=raw_info["commit"],
|
||||
date=date_info,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,881 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Any, Union, overload
|
||||
|
||||
from .asset import Asset
|
||||
from .colour import Colour
|
||||
from .enums import ActivityType, try_enum
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .utils import _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
"BaseActivity",
|
||||
"Activity",
|
||||
"Streaming",
|
||||
"Game",
|
||||
"Spotify",
|
||||
"CustomActivity",
|
||||
)
|
||||
|
||||
"""If you're 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[dict]
|
||||
label: str (max: 32)
|
||||
url: str (max: 512)
|
||||
NOTE: Bots cannot access a user's activity button URLs. When received through the
|
||||
gateway, the type of the buttons field will be list[str].
|
||||
|
||||
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 .types.activity import Activity as ActivityPayload
|
||||
from .types.activity import ActivityAssets, ActivityParty, ActivityTimestamps
|
||||
|
||||
|
||||
class 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__ = ("_created_at",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._created_at: float | None = kwargs.pop("created_at", None)
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime | None:
|
||||
"""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
|
||||
)
|
||||
|
||||
def to_dict(self) -> ActivityPayload:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
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`
|
||||
|
||||
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 party status or text used for a custom status.
|
||||
details: Optional[:class:`str`]
|
||||
The detail of the user's current activity.
|
||||
timestamps: Dict[:class:`str`, :class:`int`]
|
||||
A dictionary of timestamps. It contains the following optional keys:
|
||||
|
||||
- ``start``: Corresponds to when the user started doing the
|
||||
activity in milliseconds since Unix epoch.
|
||||
- ``end``: Corresponds to when the user will finish doing the
|
||||
activity in milliseconds since Unix epoch.
|
||||
|
||||
assets: Dict[:class:`str`, :class:`str`]
|
||||
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.
|
||||
- ``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.
|
||||
|
||||
party: Dict[:class:`str`, Union[:class:`str`, List[:class:`int`]]]
|
||||
A dictionary representing the activity party. It contains the following optional keys:
|
||||
|
||||
- ``id``: A string representing the party ID.
|
||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
||||
buttons: Union[List[Dict[:class:`str`, :class:`str`]], List[:class:`str`]]
|
||||
A list of dictionaries representing custom buttons shown in a rich presence.
|
||||
Each dictionary contains the following keys:
|
||||
|
||||
- ``label``: A string representing the text shown on the button.
|
||||
- ``url``: A string representing the URL opened upon clicking the button.
|
||||
|
||||
.. note::
|
||||
|
||||
Bots cannot access a user's activity button URLs. Therefore, the type of this attribute
|
||||
will be List[:class:`str`] when received through the gateway.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
emoji: Optional[:class:`PartialEmoji`]
|
||||
The emoji that belongs to this activity.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"state",
|
||||
"details",
|
||||
"_created_at",
|
||||
"timestamps",
|
||||
"assets",
|
||||
"party",
|
||||
"flags",
|
||||
"sync_id",
|
||||
"session_id",
|
||||
"type",
|
||||
"name",
|
||||
"url",
|
||||
"application_id",
|
||||
"emoji",
|
||||
"buttons",
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.state: str | None = kwargs.pop("state", None)
|
||||
self.details: str | None = kwargs.pop("details", None)
|
||||
self.timestamps: ActivityTimestamps = kwargs.pop("timestamps", {})
|
||||
self.assets: ActivityAssets = kwargs.pop("assets", {})
|
||||
self.party: ActivityParty = kwargs.pop("party", {})
|
||||
self.application_id: int | None = _get_as_snowflake(kwargs, "application_id")
|
||||
self.url: str | None = kwargs.pop("url", None)
|
||||
self.flags: int = kwargs.pop("flags", 0)
|
||||
self.sync_id: str | None = kwargs.pop("sync_id", None)
|
||||
self.session_id: str | None = kwargs.pop("session_id", None)
|
||||
self.buttons: list[str] = kwargs.pop("buttons", [])
|
||||
|
||||
activity_type = kwargs.pop("type", -1)
|
||||
self.type: ActivityType = (
|
||||
activity_type
|
||||
if isinstance(activity_type, ActivityType)
|
||||
else try_enum(ActivityType, activity_type)
|
||||
)
|
||||
self.name: str | None = kwargs.pop(
|
||||
"name", "Custom Status" if self.type == ActivityType.custom else None
|
||||
)
|
||||
|
||||
emoji = kwargs.pop("emoji", None)
|
||||
self.emoji: PartialEmoji | None = (
|
||||
PartialEmoji.from_dict(emoji) if emoji is not None else None
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = (
|
||||
("type", self.type),
|
||||
("name", self.name),
|
||||
("state", self.state),
|
||||
("url", self.url),
|
||||
("details", self.details),
|
||||
("application_id", self.application_id),
|
||||
("session_id", self.session_id),
|
||||
("emoji", self.emoji),
|
||||
)
|
||||
inner = " ".join("%s=%r" % t for t 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
|
||||
ret["type"] = int(self.type)
|
||||
if self.emoji:
|
||||
ret["emoji"] = self.emoji.to_dict()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime | None:
|
||||
"""When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps["start"] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def end(self) -> datetime.datetime | None:
|
||||
"""When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps["end"] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def large_image_url(self) -> str | None:
|
||||
"""Returns a URL pointing to the large image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
large_image = self.assets["large_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return f"{Asset.BASE}/app-assets/{self.application_id}/{large_image}.png"
|
||||
|
||||
@property
|
||||
def small_image_url(self) -> str | None:
|
||||
"""Returns a URL pointing to the small image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
small_image = self.assets["small_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return f"{Asset.BASE}/app-assets/{self.application_id}/{small_image}.png"
|
||||
|
||||
@property
|
||||
def large_image_text(self) -> str | None:
|
||||
"""Returns the large image asset hover text of this activity if applicable."""
|
||||
return self.assets.get("large_text", None)
|
||||
|
||||
@property
|
||||
def small_image_text(self) -> str | None:
|
||||
"""Returns the small image asset hover text of this activity if applicable."""
|
||||
return self.assets.get("small_text", 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.
|
||||
|
||||
.. container:: 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.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "_end", "_start")
|
||||
|
||||
def __init__(self, name: str, **extra):
|
||||
super().__init__(**extra)
|
||||
self.name: str = name
|
||||
|
||||
try:
|
||||
timestamps: ActivityTimestamps = extra["timestamps"]
|
||||
except KeyError:
|
||||
self._start = 0
|
||||
self._end = 0
|
||||
else:
|
||||
self._start = timestamps.get("start", 0)
|
||||
self._end = timestamps.get("end", 0)
|
||||
|
||||
@property
|
||||
def type(self) -> ActivityType:
|
||||
"""Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.playing`.
|
||||
"""
|
||||
return ActivityType.playing
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime | None:
|
||||
"""When the user started playing this game in UTC, if applicable."""
|
||||
if self._start:
|
||||
return datetime.datetime.fromtimestamp(
|
||||
self._start / 1000, tz=datetime.timezone.utc
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self) -> datetime.datetime | None:
|
||||
"""When the user will stop playing this game in UTC, if applicable."""
|
||||
if self._end:
|
||||
return datetime.datetime.fromtimestamp(
|
||||
self._end / 1000, tz=datetime.timezone.utc
|
||||
)
|
||||
return None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Game name={self.name!r}>"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
timestamps: dict[str, Any] = {}
|
||||
if self._start:
|
||||
timestamps["start"] = self._start
|
||||
|
||||
if self._end:
|
||||
timestamps["end"] = self._end
|
||||
|
||||
return {
|
||||
"type": ActivityType.playing.value,
|
||||
"name": str(self.name),
|
||||
"timestamps": timestamps,
|
||||
}
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Game) and other.name == self.name
|
||||
|
||||
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.
|
||||
|
||||
.. container:: 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: Dict[:class:`str`, :class:`str`]
|
||||
A dictionary comprised of similar keys than those in :attr:`Activity.assets`.
|
||||
"""
|
||||
|
||||
__slots__ = ("platform", "name", "game", "url", "details", "assets")
|
||||
|
||||
def __init__(self, *, name: str | None, url: str, **extra: Any):
|
||||
super().__init__(**extra)
|
||||
self.platform: str | None = name
|
||||
self.name: str | None = extra.pop("details", name)
|
||||
self.game: str | None = extra.pop("state", None)
|
||||
self.url: str = url
|
||||
self.details: str | None = extra.pop("details", self.name) # compatibility
|
||||
self.assets: ActivityAssets = extra.pop("assets", {})
|
||||
|
||||
@property
|
||||
def type(self) -> 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) -> str | None:
|
||||
"""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 this is 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 __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Spotify:
|
||||
"""Represents a Spotify listening activity from Discord. This is a special case of
|
||||
:class:`Activity` that makes it easier to work with the Spotify integration.
|
||||
|
||||
.. container:: 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",
|
||||
"_timestamps",
|
||||
"_assets",
|
||||
"_party",
|
||||
"_sync_id",
|
||||
"_session_id",
|
||||
"_created_at",
|
||||
)
|
||||
|
||||
def __init__(self, **data):
|
||||
self._state: str = data.pop("state", "")
|
||||
self._details: str = data.pop("details", "")
|
||||
self._timestamps: dict[str, int] = data.pop("timestamps", {})
|
||||
self._assets: ActivityAssets = data.pop("assets", {})
|
||||
self._party: ActivityParty = data.pop("party", {})
|
||||
self._sync_id: str = data.pop("sync_id")
|
||||
self._session_id: str = data.pop("session_id")
|
||||
self._created_at: float | None = data.pop("created_at", None)
|
||||
|
||||
@property
|
||||
def type(self) -> ActivityType:
|
||||
"""Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.listening`.
|
||||
"""
|
||||
return ActivityType.listening
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime | None:
|
||||
"""When the user started listening 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 colour(self) -> 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:
|
||||
"""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:
|
||||
"""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 __hash__(self) -> int:
|
||||
return hash(self._session_id)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Spotify"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"<Spotify"
|
||||
f" title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""The title of the song being played."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def artists(self) -> list[str]:
|
||||
"""The artists of the song being played."""
|
||||
return self._state.split("; ")
|
||||
|
||||
@property
|
||||
def artist(self) -> 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:
|
||||
"""The album that the song being played belongs to."""
|
||||
return self._assets.get("large_text", "")
|
||||
|
||||
@property
|
||||
def album_cover_url(self) -> 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:
|
||||
"""The track ID used by Spotify to identify this song."""
|
||||
return self._sync_id
|
||||
|
||||
@property
|
||||
def track_url(self) -> str:
|
||||
"""The track URL to listen on Spotify.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return f"https://open.spotify.com/track/{self.track_id}"
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime:
|
||||
"""When the user started playing this song in UTC."""
|
||||
return datetime.datetime.fromtimestamp(
|
||||
self._timestamps["start"] / 1000, tz=datetime.timezone.utc
|
||||
)
|
||||
|
||||
@property
|
||||
def end(self) -> datetime.datetime:
|
||||
"""When the user will stop playing this song in UTC."""
|
||||
return datetime.datetime.fromtimestamp(
|
||||
self._timestamps["end"] / 1000, tz=datetime.timezone.utc
|
||||
)
|
||||
|
||||
@property
|
||||
def duration(self) -> datetime.timedelta:
|
||||
"""The duration of the song being played."""
|
||||
return self.end - self.start
|
||||
|
||||
@property
|
||||
def party_id(self) -> str:
|
||||
"""The party ID of the listening party."""
|
||||
return self._party.get("id", "")
|
||||
|
||||
|
||||
class CustomActivity(BaseActivity):
|
||||
"""Represents a Custom activity from Discord.
|
||||
|
||||
.. container:: 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.
|
||||
state: Optional[:class:`str`]
|
||||
The text used for the custom activity.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "emoji", "state")
|
||||
|
||||
def __init__(
|
||||
self, name: str | None, *, emoji: PartialEmoji | None = None, **extra: Any
|
||||
):
|
||||
super().__init__(**extra)
|
||||
self.name: str | None = name
|
||||
self.state: str | None = extra.pop("state", name)
|
||||
if self.name == "Custom Status":
|
||||
self.name = self.state
|
||||
|
||||
self.emoji: PartialEmoji | None
|
||||
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(
|
||||
"Expected str, PartialEmoji, or None, received"
|
||||
f" {type(emoji)!r} instead."
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self) -> 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) -> dict[str, Any]:
|
||||
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,
|
||||
}
|
||||
|
||||
if self.emoji:
|
||||
o["emoji"] = self.emoji.to_dict()
|
||||
return o
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
isinstance(other, CustomActivity)
|
||||
and other.name == self.name
|
||||
and other.emoji == self.emoji
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, str(self.emoji)))
|
||||
|
||||
def __str__(self) -> str:
|
||||
if not self.emoji:
|
||||
return str(self.name)
|
||||
if self.name:
|
||||
return f"{self.emoji} {self.name}"
|
||||
return str(self.emoji)
|
||||
|
||||
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: ActivityPayload) -> ActivityTypes: ...
|
||||
|
||||
|
||||
@overload
|
||||
def create_activity(data: None) -> None: ...
|
||||
|
||||
|
||||
def create_activity(data: ActivityPayload | None) -> ActivityTypes | None:
|
||||
if not data:
|
||||
return None
|
||||
|
||||
game_type = try_enum(ActivityType, data.get("type", -1))
|
||||
if game_type is ActivityType.playing:
|
||||
if "application_id" in data or "session_id" in data:
|
||||
return Activity(**data)
|
||||
return Game(**data)
|
||||
elif game_type is ActivityType.custom:
|
||||
try:
|
||||
name = data.pop("name")
|
||||
except KeyError:
|
||||
return Activity(**data)
|
||||
else:
|
||||
# we removed the name key from data already
|
||||
return CustomActivity(name=name, **data) # type: ignore
|
||||
elif game_type is ActivityType.streaming:
|
||||
if "url" in data:
|
||||
# the url won't be None here
|
||||
return Streaming(**data) # type: ignore
|
||||
return Activity(**data)
|
||||
elif (
|
||||
game_type is ActivityType.listening
|
||||
and "sync_id" in data
|
||||
and "session_id" in data
|
||||
):
|
||||
return Spotify(**data)
|
||||
return Activity(**data)
|
||||
@@ -0,0 +1,650 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
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
|
||||
from .types.appinfo import AppInstallParams as AppInstallParamsPayload
|
||||
from .types.appinfo import PartialAppInfo as PartialAppInfoPayload
|
||||
from .types.appinfo import Team as TeamPayload
|
||||
from .user import User
|
||||
|
||||
__all__ = (
|
||||
"AppInfo",
|
||||
"PartialAppInfo",
|
||||
"AppInstallParams",
|
||||
"IntegrationTypesConfig",
|
||||
"ApplicationEventWebhookStatus",
|
||||
)
|
||||
|
||||
|
||||
class AppInfo:
|
||||
"""Represents the application info for the bot provided by Discord.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The application ID.
|
||||
name: :class:`str`
|
||||
The application name.
|
||||
owner: :class:`User`
|
||||
The application owner.
|
||||
team: Optional[:class:`Team`]
|
||||
The application's team.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
description: :class:`str`
|
||||
The application 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.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
guild_id: Optional[:class:`int`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the guild to which it has been linked to.
|
||||
|
||||
.. 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
|
||||
|
||||
approximate_guild_count: Optional[:class:`int`]
|
||||
The approximate count of guilds to which the app has been added, if any.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
approximate_user_install_count: Optional[:class:`int`]
|
||||
The approximate count of users who have installed the application, if any.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
redirect_uris: Optional[List[:class:`str`]]
|
||||
The list of redirect URIs for the application, if set.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
interactions_endpoint_url: Optional[:class:`str`]
|
||||
The interactions endpoint URL for the application, if set.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
role_connections_verification_url: Optional[:class:`str`]
|
||||
The role connection verification URL for the application, if set.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
install_params: Optional[:class:`AppInstallParams`]
|
||||
The settings for the application's default in-app authorization link, if set.
|
||||
|
||||
.. versionchanged:: 2.8
|
||||
Fixed incorrect type documentation.
|
||||
|
||||
integration_types_config: Optional[:class:`IntegrationTypesConfig`]
|
||||
Per-installation context configuration for guild (``0``) and user (``1``) contexts.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
event_webhooks_url: Optional[:class:`str`]
|
||||
The URL used to receive application event webhooks, if set.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
event_webhooks_status: Optional[:class:`ApplicationEventWebhookStatus`]
|
||||
The status of event webhooks for the application, if set.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
event_webhooks_types: Optional[List[:class:`str`]]
|
||||
List of event webhook types subscribed to, if set.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
tags: Optional[List[:class:`str`]]
|
||||
The list of tags describing the content and functionality of the app, if set.
|
||||
|
||||
Maximium of 5 tags.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
custom_install_url: Optional[:class:`str`]
|
||||
The default custom authorization URL for the application, if set.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
approximate_user_authorization_count: Optional[:class:`int`]
|
||||
The approximate count of users who have authorized the application, if any.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
bot: Optional[:class:`User`]
|
||||
The bot user associated with this application, if any.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"description",
|
||||
"id",
|
||||
"name",
|
||||
"rpc_origins",
|
||||
"bot_public",
|
||||
"bot_require_code_grant",
|
||||
"owner",
|
||||
"bot",
|
||||
"_icon",
|
||||
"_summary",
|
||||
"verify_key",
|
||||
"team",
|
||||
"guild_id",
|
||||
"primary_sku_id",
|
||||
"slug",
|
||||
"_cover_image",
|
||||
"terms_of_service_url",
|
||||
"privacy_policy_url",
|
||||
"approximate_guild_count",
|
||||
"approximate_user_install_count",
|
||||
"approximate_user_authorization_count",
|
||||
"_flags",
|
||||
"redirect_uris",
|
||||
"interactions_endpoint_url",
|
||||
"role_connections_verification_url",
|
||||
"event_webhooks_url",
|
||||
"event_webhooks_status",
|
||||
"event_webhooks_types",
|
||||
"integration_types_config",
|
||||
"install_params",
|
||||
"tags",
|
||||
"custom_install_url",
|
||||
)
|
||||
|
||||
def __init__(self, state: ConnectionState, data: AppInfoPayload):
|
||||
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: str | None = data.get("icon")
|
||||
self.rpc_origins: list[str] | None = data.get("rpc_origins")
|
||||
self.bot_public: bool = data.get("bot_public", True)
|
||||
self.bot_require_code_grant: bool = data.get("bot_require_code_grant", False)
|
||||
self.owner: User | None = (
|
||||
state.create_user(owner)
|
||||
if (owner := data.get("owner")) is not None
|
||||
else None
|
||||
)
|
||||
|
||||
team: TeamPayload | None = data.get("team")
|
||||
self.team: Team | None = Team(state, team) if team else None
|
||||
|
||||
self._summary: str | None = data.get("summary")
|
||||
self.verify_key: str = data["verify_key"]
|
||||
self.bot: User | None = (
|
||||
state.create_user(bot) if (bot := data.get("bot")) is not None else None
|
||||
)
|
||||
|
||||
self.guild_id: int | None = utils._get_as_snowflake(data, "guild_id")
|
||||
|
||||
self.primary_sku_id: int | None = utils._get_as_snowflake(
|
||||
data, "primary_sku_id"
|
||||
)
|
||||
self.slug: str | None = data.get("slug")
|
||||
self._cover_image: str | None = data.get("cover_image")
|
||||
self.terms_of_service_url: str | None = data.get("terms_of_service_url")
|
||||
self.privacy_policy_url: str | None = data.get("privacy_policy_url")
|
||||
self.approximate_guild_count: int | None = data.get("approximate_guild_count")
|
||||
self.approximate_user_install_count: int | None = data.get(
|
||||
"approximate_user_install_count"
|
||||
)
|
||||
self.approximate_user_authorization_count: int | None = data.get(
|
||||
"approximate_user_authorization_count"
|
||||
)
|
||||
self._flags: int = data.get("flags", 0)
|
||||
self.redirect_uris: list[str] = data.get("redirect_uris", [])
|
||||
self.interactions_endpoint_url: str | None = data.get(
|
||||
"interactions_endpoint_url"
|
||||
)
|
||||
self.role_connections_verification_url: str | None = data.get(
|
||||
"role_connections_verification_url"
|
||||
)
|
||||
self.event_webhooks_url: str | None = data.get("event_webhooks_url")
|
||||
self.event_webhooks_status: ApplicationEventWebhookStatus | None = (
|
||||
try_enum(ApplicationEventWebhookStatus, status)
|
||||
if (status := data.get("event_webhooks_status")) is not None
|
||||
else None
|
||||
)
|
||||
self.event_webhooks_types: list[str] | None = data.get("event_webhooks_types")
|
||||
|
||||
self.install_params: AppInstallParams | None = (
|
||||
AppInstallParams(install_params)
|
||||
if (install_params := data.get("install_params")) is not None
|
||||
else None
|
||||
)
|
||||
self.tags: list[str] = data.get("tags", [])
|
||||
self.custom_install_url: str | None = data.get("custom_install_url")
|
||||
self.integration_types_config: IntegrationTypesConfig | None = (
|
||||
IntegrationTypesConfig.from_payload(data.get("integration_types_config"))
|
||||
)
|
||||
|
||||
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 flags(self) -> ApplicationFlags:
|
||||
"""The public application flags.
|
||||
|
||||
Returns an :class:`ApplicationFlags` instance.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
return ApplicationFlags._from_value(self._flags)
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
description: str | None = utils.MISSING,
|
||||
icon: bytes | None = utils.MISSING,
|
||||
cover_image: bytes | None = utils.MISSING,
|
||||
tags: list[str] | None = utils.MISSING,
|
||||
terms_of_service_url: str | None = utils.MISSING,
|
||||
privacy_policy_url: str | None = utils.MISSING,
|
||||
interactions_endpoint_url: str | None = utils.MISSING,
|
||||
role_connections_verification_url: str | None = utils.MISSING,
|
||||
install_params: AppInstallParams | None = utils.MISSING,
|
||||
custom_install_url: str | None = utils.MISSING,
|
||||
integration_types_config: IntegrationTypesConfig | None = utils.MISSING,
|
||||
flags: ApplicationFlags | None = utils.MISSING,
|
||||
event_webhooks_url: str | None = utils.MISSING,
|
||||
event_webhooks_status: ApplicationEventWebhookStatus = utils.MISSING,
|
||||
event_webhooks_types: list[str] | None = utils.MISSING,
|
||||
) -> AppInfo:
|
||||
"""|coro|
|
||||
|
||||
Edit the current application's settings.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Parameters
|
||||
----------
|
||||
description: Optional[:class:`str`]
|
||||
The new application description or ``None`` to clear.
|
||||
icon: Optional[:class:`bytes`]
|
||||
New icon image. If ``bytes`` is given it will be base64 encoded automatically. Pass ``None`` to clear.
|
||||
cover_image: Optional[:class:`bytes`]
|
||||
New cover image for the store embed. If ``bytes`` is given it will be base64 encoded automatically. Pass ``None`` to clear.
|
||||
tags: Optional[List[:class:`str`]]
|
||||
List of tags for the application (max 5). Pass ``None`` to clear.
|
||||
terms_of_service_url: Optional[:class:`str`]
|
||||
The application's Terms of Service URL. Pass ``None`` to clear.
|
||||
privacy_policy_url: Optional[:class:`str`]
|
||||
The application's Privacy Policy URL. Pass ``None`` to clear.
|
||||
interactions_endpoint_url: Optional[:class:`str`]
|
||||
The interactions endpoint callback URL. Pass ``None`` to clear.
|
||||
role_connections_verification_url: Optional[:class:`str`]
|
||||
The role connection verification URL for the application. Pass ``None`` to clear.
|
||||
install_params: Optional[:class:`AppInstallParams`]
|
||||
Settings for the application's default in-app authorization link. Pass ``None`` to clear. Omit entirely to leave unchanged.
|
||||
custom_install_url: Optional[:class:`str`]
|
||||
The default custom authorization URL for the application. Pass ``None`` to clear.
|
||||
integration_types_config: Optional[:class:`IntegrationTypesConfig`]
|
||||
Object specifying per-installation context configuration (guild and/or user). You may set contexts individually
|
||||
and omit others to leave them unchanged. Pass the object with a context explicitly set to ``None`` to clear just that
|
||||
context, or pass ``None`` to clear the entire integration types configuration.
|
||||
flags: Optional[:class:`ApplicationFlags`]
|
||||
Application public flags. Pass ``None`` to clear (not typical).
|
||||
event_webhooks_url: Optional[:class:`str`]
|
||||
Event webhooks callback URL for receiving application webhook events. Pass ``None`` to clear.
|
||||
event_webhooks_status: :class:`ApplicationEventWebhookStatus`
|
||||
The desired webhook status.
|
||||
event_webhooks_types: Optional[List[:class:`str`]]
|
||||
List of webhook event types to subscribe to. Pass ``None`` to clear.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`.AppInfo`
|
||||
The updated application information.
|
||||
"""
|
||||
payload: dict[str, Any] = {}
|
||||
if description is not utils.MISSING:
|
||||
payload["description"] = description
|
||||
if icon is not utils.MISSING:
|
||||
if icon is None:
|
||||
payload["icon"] = None
|
||||
else:
|
||||
payload["icon"] = utils._bytes_to_base64_data(icon)
|
||||
if cover_image is not utils.MISSING:
|
||||
if cover_image is None:
|
||||
payload["cover_image"] = None
|
||||
else:
|
||||
payload["cover_image"] = utils._bytes_to_base64_data(cover_image)
|
||||
if tags is not utils.MISSING:
|
||||
payload["tags"] = tags
|
||||
if terms_of_service_url is not utils.MISSING:
|
||||
payload["terms_of_service_url"] = terms_of_service_url
|
||||
if privacy_policy_url is not utils.MISSING:
|
||||
payload["privacy_policy_url"] = privacy_policy_url
|
||||
if interactions_endpoint_url is not utils.MISSING:
|
||||
payload["interactions_endpoint_url"] = interactions_endpoint_url
|
||||
if role_connections_verification_url is not utils.MISSING:
|
||||
payload["role_connections_verification_url"] = (
|
||||
role_connections_verification_url
|
||||
)
|
||||
if install_params is not utils.MISSING:
|
||||
if install_params is None:
|
||||
payload["install_params"] = None
|
||||
else:
|
||||
payload["install_params"] = install_params._to_payload()
|
||||
if custom_install_url is not utils.MISSING:
|
||||
payload["custom_install_url"] = custom_install_url
|
||||
if integration_types_config is not utils.MISSING:
|
||||
if integration_types_config is None:
|
||||
payload["integration_types_config"] = None
|
||||
else:
|
||||
payload["integration_types_config"] = (
|
||||
integration_types_config._to_payload()
|
||||
)
|
||||
if flags is not utils.MISSING:
|
||||
payload["flags"] = None if flags is None else flags.value
|
||||
if event_webhooks_url is not utils.MISSING:
|
||||
payload["event_webhooks_url"] = event_webhooks_url
|
||||
if event_webhooks_status is not utils.MISSING:
|
||||
payload["event_webhooks_status"] = event_webhooks_status.value
|
||||
if event_webhooks_types is not utils.MISSING:
|
||||
payload["event_webhooks_types"] = event_webhooks_types
|
||||
|
||||
data = await self._state.http.edit_current_application_info(payload)
|
||||
return AppInfo(self._state, data)
|
||||
|
||||
@property
|
||||
def icon(self) -> Asset | None:
|
||||
"""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) -> Asset | None:
|
||||
"""Retrieves the cover image on a store embed, if any.
|
||||
|
||||
This is only available if the application is a game sold on Discord.
|
||||
"""
|
||||
if self._cover_image is None:
|
||||
return None
|
||||
return Asset._from_cover_image(self._state, self.id, self._cover_image)
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild | None:
|
||||
"""If this application is a game sold on Discord,
|
||||
this field will be the guild to which it has been linked.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def summary(self) -> str | None:
|
||||
"""If this application is a game sold on Discord,
|
||||
this field will be the summary field for the store page of its primary SKU.
|
||||
|
||||
It currently returns an empty string.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
.. deprecated:: 2.7
|
||||
"""
|
||||
utils.warn_deprecated(
|
||||
"summary",
|
||||
"description",
|
||||
reference="https://docs.discord.com/developers/resources/application#application-object-application-structure",
|
||||
)
|
||||
return self._summary
|
||||
|
||||
|
||||
class PartialAppInfo:
|
||||
"""Represents a partial AppInfo given by :func:`~discord.abc.GuildChannel.create_invite`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The application ID.
|
||||
name: :class:`str`
|
||||
The application name.
|
||||
description: :class:`str`
|
||||
The application description.
|
||||
rpc_origins: Optional[List[:class:`str`]]
|
||||
A list of RPC origin URLs, if RPC is enabled.
|
||||
summary: :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.
|
||||
verify_key: :class:`str`
|
||||
The hex encoded key for verification in interactions.
|
||||
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):
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self._icon: str | None = data.get("icon")
|
||||
self.description: str = data["description"]
|
||||
self.rpc_origins: list[str] | None = data.get("rpc_origins")
|
||||
self.summary: str = data["summary"]
|
||||
self.verify_key: str = data["verify_key"]
|
||||
self.terms_of_service_url: str | None = data.get("terms_of_service_url")
|
||||
self.privacy_policy_url: str | None = 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) -> Asset | None:
|
||||
"""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")
|
||||
|
||||
|
||||
class AppInstallParams:
|
||||
"""Represents the settings for the custom authorization URL of an application.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Attributes
|
||||
----------
|
||||
scopes: List[:class:`str`]
|
||||
The list of OAuth2 scopes for adding the application to a guild.
|
||||
permissions: :class:`Permissions`
|
||||
The permissions to request for the bot role in the guild.
|
||||
"""
|
||||
|
||||
__slots__ = ("scopes", "permissions")
|
||||
|
||||
def __init__(self, data: AppInstallParamsPayload) -> None:
|
||||
self.scopes: list[str] = data.get("scopes", [])
|
||||
self.permissions: Permissions = Permissions(int(data["permissions"]))
|
||||
|
||||
def _to_payload(self) -> dict[str, object]:
|
||||
"""Serialize this object into an application install params payload.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Any]
|
||||
A dict with ``scopes`` and ``permissions`` (string form) suitable for the API.
|
||||
"""
|
||||
if self.permissions.value > 0 and "bot" not in self.scopes:
|
||||
raise ValueError(
|
||||
"'bot' must be in install_params.scopes if permissions are requested"
|
||||
)
|
||||
return {
|
||||
"scopes": list(self.scopes),
|
||||
"permissions": str(self.permissions.value),
|
||||
}
|
||||
|
||||
|
||||
class IntegrationTypesConfig:
|
||||
"""Represents per-installation context configuration for an application.
|
||||
|
||||
This object is used to build the payload for the ``integration_types_config`` field when editing an application.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Parameters
|
||||
----------
|
||||
guild: Optional[:class:`AppInstallParams`]
|
||||
The configuration for the guild installation context. Omit to leave unchanged; pass ``None`` to clear.
|
||||
user: Optional[:class:`AppInstallParams`]
|
||||
The configuration for the user installation context. Omit to leave unchanged; pass ``None`` to clear.
|
||||
"""
|
||||
|
||||
__slots__ = ("guild", "user")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
guild: AppInstallParams | None = utils.MISSING,
|
||||
user: AppInstallParams | None = utils.MISSING,
|
||||
) -> None:
|
||||
self.guild: AppInstallParams | None = guild
|
||||
self.user: AppInstallParams | None = user
|
||||
|
||||
@staticmethod
|
||||
def _get_ctx(
|
||||
raw: dict[int | str, dict[str, object] | None] | None, key: int
|
||||
) -> dict[str, object] | None:
|
||||
if raw is None:
|
||||
return None
|
||||
if key in raw:
|
||||
return raw[key]
|
||||
skey = str(key)
|
||||
return raw.get(skey)
|
||||
|
||||
@staticmethod
|
||||
def _decode_ctx(value: dict[str, Any] | None) -> AppInstallParams | None:
|
||||
if value is None:
|
||||
return None
|
||||
params = value.get("oauth2_install_params")
|
||||
if not params:
|
||||
return None
|
||||
return AppInstallParams(params)
|
||||
|
||||
@classmethod
|
||||
def from_payload(
|
||||
cls, data: dict[int | str, dict[str, Any] | None] | None
|
||||
) -> IntegrationTypesConfig | None:
|
||||
if data is None:
|
||||
return None
|
||||
guild_ctx = cls._decode_ctx(cls._get_ctx(data, 0))
|
||||
user_ctx = cls._decode_ctx(cls._get_ctx(data, 1))
|
||||
return cls(guild=guild_ctx, user=user_ctx)
|
||||
|
||||
def _encode_install_params(
|
||||
self, value: AppInstallParams | None
|
||||
) -> dict[str, dict[str, Any]] | None:
|
||||
if value is None:
|
||||
return None
|
||||
return {"oauth2_install_params": value._to_payload()}
|
||||
|
||||
def _to_payload(self) -> dict[int, dict[str, dict[str, Any]] | None]:
|
||||
"""Serialize this configuration into the payload expected by the API.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[int, Dict[str, Dict[str, Any]] | None]
|
||||
Mapping of integration context IDs to encoded install parameters, or ``None`` to clear.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
payload: dict[int, dict[str, dict[str, Any]] | None] = {}
|
||||
if self.guild is not utils.MISSING:
|
||||
payload[0] = self._encode_install_params(self.guild)
|
||||
if self.user is not utils.MISSING:
|
||||
payload[1] = self._encode_install_params(self.user)
|
||||
return payload
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .enums import ApplicationRoleConnectionMetadataType, try_enum
|
||||
from .utils import MISSING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.application_role_connection import (
|
||||
ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload,
|
||||
)
|
||||
|
||||
__all__ = ("ApplicationRoleConnectionMetadata",)
|
||||
|
||||
|
||||
class ApplicationRoleConnectionMetadata:
|
||||
r"""Represents role connection metadata for a Discord application.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type: :class:`ApplicationRoleConnectionMetadataType`
|
||||
The type of metadata value.
|
||||
key: :class:`str`
|
||||
The key for this metadata field.
|
||||
May only be the ``a-z``, ``0-9``, or ``_`` characters, with a maximum of 50 characters.
|
||||
name: :class:`str`
|
||||
The name for this metadata field. Maximum 100 characters.
|
||||
description: :class:`str`
|
||||
The description for this metadata field. Maximum 200 characters.
|
||||
name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
|
||||
The name localizations for this metadata field. The values of this should be ``"locale": "name"``.
|
||||
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
|
||||
description_localizations: Optional[Dict[:class:`str`, :class:`str`]]
|
||||
The description localizations for this metadata field. The values of this should be ``"locale": "name"``.
|
||||
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"type",
|
||||
"key",
|
||||
"name",
|
||||
"description",
|
||||
"name_localizations",
|
||||
"description_localizations",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: ApplicationRoleConnectionMetadataType,
|
||||
key: str,
|
||||
name: str,
|
||||
description: str,
|
||||
name_localizations: dict[str, str] = MISSING,
|
||||
description_localizations: dict[str, str] = MISSING,
|
||||
):
|
||||
self.type: ApplicationRoleConnectionMetadataType = type
|
||||
self.key: str = key
|
||||
self.name: str = name
|
||||
self.name_localizations: dict[str, str] = name_localizations
|
||||
self.description: str = description
|
||||
self.description_localizations: dict[str, str] = description_localizations
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<ApplicationRoleConnectionMetadata "
|
||||
f"type={self.type!r} "
|
||||
f"key={self.key!r} "
|
||||
f"name={self.name!r} "
|
||||
f"description={self.description!r} "
|
||||
f"name_localizations={self.name_localizations!r} "
|
||||
f"description_localizations={self.description_localizations!r}>"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, data: ApplicationRoleConnectionMetadataPayload
|
||||
) -> ApplicationRoleConnectionMetadata:
|
||||
return cls(
|
||||
type=try_enum(ApplicationRoleConnectionMetadataType, data["type"]),
|
||||
key=data["key"],
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
name_localizations=data.get("name_localizations"),
|
||||
description_localizations=data.get("description_localizations"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> ApplicationRoleConnectionMetadataPayload:
|
||||
data = {
|
||||
"type": self.type.value,
|
||||
"key": self.key,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
}
|
||||
if self.name_localizations is not MISSING:
|
||||
data["name_localizations"] = self.name_localizations
|
||||
if self.description_localizations is not MISSING:
|
||||
data["description_localizations"] = self.description_localizations
|
||||
return data
|
||||
@@ -0,0 +1,526 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import yarl
|
||||
|
||||
from . import utils
|
||||
from .errors import DiscordException, InvalidArgument
|
||||
|
||||
__all__ = ("Asset",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"]
|
||||
ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"]
|
||||
from .state import ConnectionState
|
||||
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
|
||||
MISSING = utils.MISSING
|
||||
|
||||
|
||||
class AssetMixin:
|
||||
url: str
|
||||
_state: Any | None
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
"""
|
||||
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: 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 attachment 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.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Asset(AssetMixin):
|
||||
"""Represents a CDN asset on Discord.
|
||||
|
||||
.. container:: 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 asset's url's hash.
|
||||
|
||||
This is equivalent to hash(:attr:`url`).
|
||||
"""
|
||||
|
||||
__slots__: tuple[str, ...] = (
|
||||
"_state",
|
||||
"_url",
|
||||
"_animated",
|
||||
"_key",
|
||||
)
|
||||
|
||||
BASE = "https://cdn.discordapp.com"
|
||||
|
||||
def __init__(self, state, *, url: str, key: str, animated: bool = False):
|
||||
self._state = state
|
||||
self._url = url
|
||||
self._animated = animated
|
||||
self._key = key
|
||||
|
||||
@classmethod
|
||||
def _from_default_avatar(cls, state, index: int) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/embed/avatars/{index}.png",
|
||||
key=str(index),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
|
||||
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_avatar_decoration(
|
||||
cls, state, user_id: int, avatar_decoration: str
|
||||
) -> Asset:
|
||||
animated = avatar_decoration.startswith("a_")
|
||||
endpoint = (
|
||||
"avatar-decoration-presets"
|
||||
# if avatar_decoration.startswith(("v3", "v2"))
|
||||
# else f"avatar-decorations/{user_id}"
|
||||
)
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{endpoint}/{avatar_decoration}.png?size=1024",
|
||||
key=avatar_decoration,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_user_primary_guild_tag(
|
||||
cls, state: ConnectionState, identity_guild_id: int, badge_id: str
|
||||
) -> Asset:
|
||||
"""Creates an Asset for a user's primary guild (tag) badge.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state: ConnectionState
|
||||
The connection state.
|
||||
identity_guild_id: int
|
||||
The ID of the guild.
|
||||
badge_id: str
|
||||
The badge hash/id.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The primary guild badge asset.
|
||||
"""
|
||||
return cls(
|
||||
state,
|
||||
url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256",
|
||||
key=badge_id,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_avatar(
|
||||
cls, state, guild_id: int, member_id: int, avatar: str
|
||||
) -> Asset:
|
||||
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, guild_id: int, member_id: int, banner: str
|
||||
) -> Asset:
|
||||
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=512",
|
||||
key=banner,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
|
||||
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, object_id: int, cover_image_hash: str) -> Asset:
|
||||
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_collectible(
|
||||
cls, state: ConnectionState, asset: str, animated: bool = False
|
||||
) -> Asset:
|
||||
name = "static.png" if not animated else "asset.webm"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/assets/collectibles/{asset}{name}",
|
||||
key=asset,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
|
||||
animated = False
|
||||
format = "png"
|
||||
if path == "banners":
|
||||
animated = image.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024",
|
||||
key=image,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
|
||||
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, banner: int) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png",
|
||||
key=str(banner),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset:
|
||||
animated = banner_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512",
|
||||
key=banner_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_scheduled_event_image(
|
||||
cls, state, event_id: int, cover_hash: str
|
||||
) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guild-events/{event_id}/{cover_hash}.png",
|
||||
key=cover_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_soundboard_sound(cls, state, sound_id: int) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/soundboard-sounds/{sound_id}",
|
||||
key=str(sound_id),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._url
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._url)
|
||||
|
||||
def __repr__(self):
|
||||
shorten = self._url.replace(self.BASE, "")
|
||||
return f"<Asset url={shorten!r}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._url)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Returns the underlying URL of the asset."""
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Returns the identifying key of the asset."""
|
||||
return self._key
|
||||
|
||||
def is_animated(self) -> bool:
|
||||
"""Returns whether the asset is animated."""
|
||||
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.
|
||||
|
||||
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'.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
An invalid size or format was passed.
|
||||
"""
|
||||
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 InvalidArgument(
|
||||
f"format must be one of {VALID_ASSET_FORMATS}"
|
||||
)
|
||||
url = url.with_path(f"{path}.{format}")
|
||||
elif static_format is MISSING:
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(
|
||||
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 InvalidArgument(
|
||||
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 InvalidArgument("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(url)
|
||||
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_size(self, size: int, /) -> Asset:
|
||||
"""Returns a new asset with the specified size.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
The asset had an invalid size.
|
||||
"""
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
format: :class:`str`
|
||||
The new format of the asset.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
The asset has an invalid format.
|
||||
"""
|
||||
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}")
|
||||
elif format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
url = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string))
|
||||
return Asset(state=self._state, url=url, 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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
format: :class:`str`
|
||||
The new static format of the asset.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
The asset had an invalid format.
|
||||
"""
|
||||
|
||||
if self._animated:
|
||||
return self
|
||||
return self.with_format(format)
|
||||
@@ -0,0 +1,719 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, TypeVar
|
||||
|
||||
from . import enums, utils
|
||||
from .asset import Asset
|
||||
from .automod import AutoModAction, AutoModTriggerMetadata
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
|
||||
__all__ = (
|
||||
"AuditLogDiff",
|
||||
"AuditLogChanges",
|
||||
"AuditLogEntry",
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import abc
|
||||
from .emoji import GuildEmoji
|
||||
from .guild import Guild
|
||||
from .member import Member
|
||||
from .role import Role
|
||||
from .scheduled_events import ScheduledEvent
|
||||
from .stage_instance import StageInstance
|
||||
from .state import ConnectionState
|
||||
from .sticker import GuildSticker
|
||||
from .threads import Thread
|
||||
from .types.audit_log import AuditLogChange as AuditLogChangePayload
|
||||
from .types.audit_log import AuditLogEntry as AuditLogEntryPayload
|
||||
from .types.automod import AutoModAction as AutoModActionPayload
|
||||
from .types.automod import AutoModTriggerMetadata as AutoModTriggerMetadataPayload
|
||||
from .types.channel import PermissionOverwrite as PermissionOverwritePayload
|
||||
from .types.role import Role as RolePayload
|
||||
from .types.snowflake import Snowflake
|
||||
from .user import User
|
||||
|
||||
|
||||
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: Snowflake | None
|
||||
) -> abc.GuildChannel | Object | None:
|
||||
if data is None:
|
||||
return None
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
|
||||
def _transform_channels(
|
||||
entry: AuditLogEntry, data: list[Snowflake] | None
|
||||
) -> list[abc.GuildChannel | Object] | None:
|
||||
if data is None:
|
||||
return None
|
||||
return [_transform_channel(entry, channel) for channel in data]
|
||||
|
||||
|
||||
def _transform_roles(
|
||||
entry: AuditLogEntry, data: list[Snowflake] | None
|
||||
) -> list[Role | Object] | None:
|
||||
if data is None:
|
||||
return None
|
||||
return [entry.guild.get_role(int(r)) or Object(id=r) for r in data]
|
||||
|
||||
|
||||
def _transform_member_id(
|
||||
entry: AuditLogEntry, data: Snowflake | None
|
||||
) -> Member | User | None:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
|
||||
def _transform_guild_id(entry: AuditLogEntry, data: Snowflake | None) -> Guild | None:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._state._get_guild(data)
|
||||
|
||||
|
||||
def _transform_overwrites(
|
||||
entry: AuditLogEntry, data: list[PermissionOverwritePayload]
|
||||
) -> list[tuple[Object, PermissionOverwrite]]:
|
||||
overwrites = []
|
||||
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: str | None) -> Asset | None:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
|
||||
|
||||
|
||||
def _transform_avatar(entry: AuditLogEntry, data: str | None) -> Asset | None:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore
|
||||
|
||||
|
||||
def _transform_scheduled_event_image(
|
||||
entry: AuditLogEntry, data: str | None
|
||||
) -> Asset | None:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_scheduled_event_image(entry._state, entry._target_id, data)
|
||||
|
||||
|
||||
def _guild_hash_transformer(
|
||||
path: str,
|
||||
) -> Callable[[AuditLogEntry, str | None], Asset | None]:
|
||||
def _transform(entry: AuditLogEntry, data: str | None) -> Asset | None:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
T = TypeVar("T", bound=enums.Enum)
|
||||
|
||||
|
||||
def _enum_transformer(enum: type[T]) -> Callable[[AuditLogEntry, int], T]:
|
||||
def _transform(entry: AuditLogEntry, data: int) -> T:
|
||||
return enums.try_enum(enum, data)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
def _transform_type(
|
||||
entry: AuditLogEntry, data: int
|
||||
) -> enums.ChannelType | enums.StickerType:
|
||||
if entry.action.name.startswith("sticker_"):
|
||||
return enums.try_enum(enums.StickerType, data)
|
||||
else:
|
||||
return enums.try_enum(enums.ChannelType, data)
|
||||
|
||||
|
||||
def _transform_actions(
|
||||
entry: AuditLogEntry, data: list[AutoModActionPayload] | None
|
||||
) -> list[AutoModAction] | None:
|
||||
if data is None:
|
||||
return None
|
||||
else:
|
||||
return [AutoModAction.from_dict(d) for d in data]
|
||||
|
||||
|
||||
def _transform_trigger_metadata(
|
||||
entry: AuditLogEntry, data: AutoModTriggerMetadataPayload | None
|
||||
) -> AutoModTriggerMetadata | None:
|
||||
if data is None:
|
||||
return None
|
||||
else:
|
||||
return AutoModTriggerMetadata.from_dict(data)
|
||||
|
||||
|
||||
def _transform_communication_disabled_until(
|
||||
entry: AuditLogEntry, data: str
|
||||
) -> datetime.datetime | None:
|
||||
if data:
|
||||
return datetime.datetime.fromisoformat(data)
|
||||
return None
|
||||
|
||||
|
||||
class AuditLogDiff:
|
||||
def __len__(self) -> int:
|
||||
return len(self.__dict__)
|
||||
|
||||
def __iter__(self) -> Generator[tuple[str, Any]]:
|
||||
yield from self.__dict__.items()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
values = " ".join("%s=%r" % item for item 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:
|
||||
TRANSFORMERS: ClassVar[dict[str, tuple[str | None, Transformer | None]]] = {
|
||||
"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),
|
||||
"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),
|
||||
"guild_id": ("guild", _transform_guild_id),
|
||||
"tags": ("emoji", None),
|
||||
"default_message_notifications": (
|
||||
"default_notifications",
|
||||
_enum_transformer(enums.NotificationLevel),
|
||||
),
|
||||
"rtc_region": (None, _enum_transformer(enums.VoiceRegion)),
|
||||
"video_quality_mode": (None, _enum_transformer(enums.VideoQualityMode)),
|
||||
"privacy_level": (None, _enum_transformer(enums.StagePrivacyLevel)),
|
||||
"format_type": (None, _enum_transformer(enums.StickerFormatType)),
|
||||
"type": (None, _transform_type),
|
||||
"status": (None, _enum_transformer(enums.ScheduledEventStatus)),
|
||||
"entity_type": (
|
||||
"location_type",
|
||||
_enum_transformer(enums.ScheduledEventLocationType),
|
||||
),
|
||||
"command_id": ("command_id", _transform_snowflake),
|
||||
"image_hash": ("image", _transform_scheduled_event_image),
|
||||
"trigger_type": (None, _enum_transformer(enums.AutoModTriggerType)),
|
||||
"event_type": (None, _enum_transformer(enums.AutoModEventType)),
|
||||
"actions": (None, _transform_actions),
|
||||
"trigger_metadata": (None, _transform_trigger_metadata),
|
||||
"exempt_roles": (None, _transform_roles),
|
||||
"exempt_channels": (None, _transform_channels),
|
||||
"communication_disabled_until": (None, _transform_communication_disabled_until),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: AuditLogEntry,
|
||||
data: list[AuditLogChangePayload],
|
||||
*,
|
||||
state: ConnectionState,
|
||||
):
|
||||
self.before = AuditLogDiff()
|
||||
self.after = AuditLogDiff()
|
||||
|
||||
for elem in sorted(data, key=lambda i: i["key"]):
|
||||
attr = elem["key"]
|
||||
|
||||
# special cases for role/trigger_metadata 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
|
||||
elif attr in [
|
||||
"$add_keyword_filter",
|
||||
"$add_regex_patterns",
|
||||
"$add_allow_list",
|
||||
]:
|
||||
self._handle_trigger_metadata(
|
||||
self.before, self.after, entry, elem["new_value"], attr # type: ignore
|
||||
)
|
||||
continue
|
||||
elif attr in [
|
||||
"$remove_keyword_filter",
|
||||
"$remove_regex_patterns",
|
||||
"$remove_allow_list",
|
||||
]:
|
||||
self._handle_trigger_metadata(
|
||||
self.after, self.before, entry, elem["new_value"], attr # type: ignore
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
key, transformer = self.TRANSFORMERS[attr]
|
||||
except (ValueError, KeyError):
|
||||
transformer = None
|
||||
else:
|
||||
if key:
|
||||
attr = key
|
||||
|
||||
transformer: Transformer | None
|
||||
|
||||
try:
|
||||
before = elem["old_value"]
|
||||
except KeyError:
|
||||
before = None
|
||||
else:
|
||||
if transformer:
|
||||
before = transformer(entry, before)
|
||||
|
||||
if attr == "location" and hasattr(self.before, "location_type"):
|
||||
from .scheduled_events import ScheduledEventLocation
|
||||
|
||||
if (
|
||||
self.before.location_type
|
||||
is enums.ScheduledEventLocationType.external
|
||||
):
|
||||
before = ScheduledEventLocation(state=state, value=before)
|
||||
elif hasattr(self.before, "channel"):
|
||||
before = ScheduledEventLocation(
|
||||
state=state, value=self.before.channel
|
||||
)
|
||||
|
||||
setattr(self.before, attr, before)
|
||||
|
||||
try:
|
||||
after = elem["new_value"]
|
||||
except KeyError:
|
||||
after = None
|
||||
else:
|
||||
if transformer:
|
||||
after = transformer(entry, after)
|
||||
|
||||
if attr == "location" and hasattr(self.after, "location_type"):
|
||||
from .scheduled_events import ScheduledEventLocation
|
||||
|
||||
if (
|
||||
self.after.location_type
|
||||
is enums.ScheduledEventLocationType.external
|
||||
):
|
||||
after = ScheduledEventLocation(state=state, value=after)
|
||||
elif hasattr(self.after, "channel"):
|
||||
after = ScheduledEventLocation(
|
||||
state=state, value=self.after.channel
|
||||
)
|
||||
|
||||
setattr(self.after, attr, after)
|
||||
|
||||
# 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"):
|
||||
setattr(first, "roles", [])
|
||||
|
||||
data = []
|
||||
g: Guild = entry.guild # type: ignore
|
||||
|
||||
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)
|
||||
|
||||
setattr(second, "roles", data)
|
||||
|
||||
def _handle_trigger_metadata(
|
||||
self,
|
||||
first: AuditLogDiff,
|
||||
second: AuditLogDiff,
|
||||
entry: AuditLogEntry,
|
||||
elem: list[AutoModTriggerMetadataPayload],
|
||||
attr: str,
|
||||
) -> None:
|
||||
if not hasattr(first, "trigger_metadata"):
|
||||
setattr(first, "trigger_metadata", None)
|
||||
|
||||
key = attr.split("_", 1)[-1]
|
||||
data = {key: elem}
|
||||
tm = AutoModTriggerMetadata.from_dict(data)
|
||||
|
||||
setattr(second, "trigger_metadata", tm)
|
||||
|
||||
|
||||
class _AuditLogProxyMemberPrune:
|
||||
delete_member_days: int
|
||||
members_removed: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberMoveOrMessageDelete:
|
||||
channel: abc.GuildChannel
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberDisconnect:
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyPinAction:
|
||||
channel: abc.GuildChannel
|
||||
message_id: int
|
||||
|
||||
|
||||
class _AuditLogProxyStageInstanceAction:
|
||||
channel: abc.GuildChannel
|
||||
|
||||
|
||||
class AuditLogEntry(Hashable):
|
||||
r"""Represents an Audit Log entry.
|
||||
|
||||
You retrieve these via :meth:`Guild.audit_logs`.
|
||||
|
||||
.. container:: 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[:class:`abc.User`]
|
||||
The user who initiated this action. Usually a :class:`Member`\, unless gone
|
||||
then it's a :class:`User`.
|
||||
id: :class:`int`
|
||||
The entry ID.
|
||||
target: Any
|
||||
The target that got changed. The exact type of this depends on
|
||||
the action being done.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason this action was 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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *, users: dict[int, User], data: AuditLogEntryPayload, guild: Guild
|
||||
):
|
||||
self._state = guild._state
|
||||
self.guild = guild
|
||||
self._users = users
|
||||
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 = data.get("options")
|
||||
|
||||
if isinstance(self.action, enums.AuditLogAction) and self.extra:
|
||||
if self.action is enums.AuditLogAction.member_prune:
|
||||
# member prune has two keys with useful information
|
||||
self.extra: _AuditLogProxyMemberPrune = type(
|
||||
"_AuditLogProxy", (), {k: int(v) for k, v in self.extra.items()}
|
||||
)()
|
||||
elif (
|
||||
self.action is enums.AuditLogAction.member_move
|
||||
or self.action is enums.AuditLogAction.message_delete
|
||||
):
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {
|
||||
"count": int(self.extra["count"]),
|
||||
"channel": self.guild.get_channel(channel_id)
|
||||
or Object(id=channel_id),
|
||||
}
|
||||
self.extra: _AuditLogProxyMemberMoveOrMessageDelete = type(
|
||||
"_AuditLogProxy", (), elems
|
||||
)()
|
||||
elif self.action is enums.AuditLogAction.member_disconnect:
|
||||
# The member disconnect action has a dict with some information
|
||||
elems = {
|
||||
"count": int(self.extra["count"]),
|
||||
}
|
||||
self.extra: _AuditLogProxyMemberDisconnect = type(
|
||||
"_AuditLogProxy", (), elems
|
||||
)()
|
||||
elif self.action.name.endswith("pin"):
|
||||
# the pin actions have a dict with some information
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {
|
||||
"channel": self.guild.get_channel(channel_id)
|
||||
or Object(id=channel_id),
|
||||
"message_id": int(self.extra["message_id"]),
|
||||
}
|
||||
self.extra: _AuditLogProxyPinAction = type(
|
||||
"_AuditLogProxy", (), elems
|
||||
)()
|
||||
elif self.action.name.startswith("overwrite_"):
|
||||
# the overwrite_ actions have a dict with some information
|
||||
instance_id = int(self.extra["id"])
|
||||
the_type = self.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 = self.extra.get("role_name") # type: ignore
|
||||
self.extra: Role = role
|
||||
elif self.action.name.startswith("stage_instance"):
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {
|
||||
"channel": self.guild.get_channel(channel_id)
|
||||
or Object(id=channel_id)
|
||||
}
|
||||
self.extra: _AuditLogProxyStageInstanceAction = type(
|
||||
"_AuditLogProxy", (), elems
|
||||
)()
|
||||
|
||||
self.extra: (
|
||||
_AuditLogProxyMemberPrune
|
||||
| _AuditLogProxyMemberMoveOrMessageDelete
|
||||
| _AuditLogProxyMemberDisconnect
|
||||
| _AuditLogProxyPinAction
|
||||
| _AuditLogProxyStageInstanceAction
|
||||
| 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")) # type: ignore
|
||||
self._target_id = utils._get_as_snowflake(data, "target_id")
|
||||
|
||||
def _get_member(self, user_id: int) -> Member | User | None:
|
||||
return self.guild.get_member(user_id) or self._users.get(user_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime:
|
||||
"""Returns the entry's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def target(
|
||||
self,
|
||||
) -> (
|
||||
Guild
|
||||
| abc.GuildChannel
|
||||
| Member
|
||||
| User
|
||||
| Role
|
||||
| Invite
|
||||
| GuildEmoji
|
||||
| StageInstance
|
||||
| GuildSticker
|
||||
| Thread
|
||||
| Object
|
||||
| None
|
||||
):
|
||||
try:
|
||||
converter = getattr(self, f"_convert_target_{self.action.target_type}")
|
||||
except AttributeError:
|
||||
return Object(id=self._target_id)
|
||||
else:
|
||||
return converter(self._target_id)
|
||||
|
||||
@property
|
||||
def category(self) -> enums.AuditLogActionCategory:
|
||||
"""The category of the action, if applicable."""
|
||||
return self.action.category
|
||||
|
||||
@cached_property
|
||||
def changes(self) -> AuditLogChanges:
|
||||
"""The list of changes this entry has."""
|
||||
obj = AuditLogChanges(self, self._changes, state=self._state)
|
||||
del self._changes
|
||||
return obj
|
||||
|
||||
@property
|
||||
def before(self) -> AuditLogDiff:
|
||||
"""The target's prior state."""
|
||||
return self.changes.before
|
||||
|
||||
@property
|
||||
def after(self) -> 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) -> abc.GuildChannel | Object:
|
||||
return self.guild.get_channel(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_user(self, target_id: int) -> Member | User | None:
|
||||
return self._get_member(target_id)
|
||||
|
||||
def _convert_target_role(self, target_id: int) -> 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 = {
|
||||
"max_age": changeset.max_age,
|
||||
"max_uses": changeset.max_uses,
|
||||
"code": changeset.code,
|
||||
"temporary": changeset.temporary,
|
||||
"uses": changeset.uses,
|
||||
}
|
||||
|
||||
obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore
|
||||
try:
|
||||
obj.inviter = changeset.inviter
|
||||
except AttributeError:
|
||||
pass
|
||||
return obj
|
||||
|
||||
def _convert_target_emoji(self, target_id: int) -> GuildEmoji | Object:
|
||||
return self._state.get_emoji(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_message(self, target_id: int) -> Member | User | None:
|
||||
return self._get_member(target_id)
|
||||
|
||||
def _convert_target_stage_instance(self, target_id: int) -> StageInstance | Object:
|
||||
return self.guild.get_stage_instance(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_sticker(self, target_id: int) -> GuildSticker | Object:
|
||||
return self._state.get_sticker(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_thread(self, target_id: int) -> Thread | Object:
|
||||
return self.guild.get_thread(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_scheduled_event(
|
||||
self, target_id: int
|
||||
) -> ScheduledEvent | Object:
|
||||
return self.guild.get_scheduled_event(target_id) or Object(id=target_id)
|
||||
@@ -0,0 +1,560 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import utils
|
||||
from .enums import (
|
||||
AutoModActionType,
|
||||
AutoModEventType,
|
||||
AutoModKeywordPresetType,
|
||||
AutoModTriggerType,
|
||||
try_enum,
|
||||
)
|
||||
from .mixins import Hashable
|
||||
from .object import Object
|
||||
|
||||
__all__ = (
|
||||
"AutoModRule",
|
||||
"AutoModAction",
|
||||
"AutoModActionMetadata",
|
||||
"AutoModTriggerMetadata",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .abc import Snowflake
|
||||
from .channel import ForumChannel, TextChannel, VoiceChannel
|
||||
from .guild import Guild
|
||||
from .member import Member
|
||||
from .role import Role
|
||||
from .state import ConnectionState
|
||||
from .types.automod import AutoModAction as AutoModActionPayload
|
||||
from .types.automod import AutoModActionMetadata as AutoModActionMetadataPayload
|
||||
from .types.automod import AutoModRule as AutoModRulePayload
|
||||
from .types.automod import AutoModTriggerMetadata as AutoModTriggerMetadataPayload
|
||||
|
||||
MISSING = utils.MISSING
|
||||
|
||||
|
||||
class AutoModActionMetadata:
|
||||
"""Represents an action's metadata.
|
||||
|
||||
Depending on the action's type, different attributes will be used.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
channel_id: :class:`int`
|
||||
The ID of the channel to send the message to.
|
||||
Only for actions of type :attr:`AutoModActionType.send_alert_message`.
|
||||
timeout_duration: :class:`datetime.timedelta`
|
||||
How long the member that triggered the action should be timed out for.
|
||||
Only for actions of type :attr:`AutoModActionType.timeout`.
|
||||
custom_message: :class:`str`
|
||||
An additional message shown to members when their message is blocked.
|
||||
Maximum 150 characters.
|
||||
Only for actions of type :attr:`AutoModActionType.block_message`.
|
||||
"""
|
||||
|
||||
# maybe add a table of action types and attributes?
|
||||
|
||||
__slots__ = (
|
||||
"channel_id",
|
||||
"timeout_duration",
|
||||
"custom_message",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_id: int = MISSING,
|
||||
timeout_duration: timedelta = MISSING,
|
||||
custom_message: str = MISSING,
|
||||
):
|
||||
self.channel_id: int = channel_id
|
||||
self.timeout_duration: timedelta = timeout_duration
|
||||
self.custom_message: str = custom_message
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
data = {}
|
||||
|
||||
if self.channel_id is not MISSING:
|
||||
data["channel_id"] = self.channel_id
|
||||
|
||||
if self.timeout_duration is not MISSING:
|
||||
data["duration_seconds"] = self.timeout_duration.total_seconds()
|
||||
|
||||
if self.custom_message is not MISSING:
|
||||
data["custom_message"] = self.custom_message
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: AutoModActionMetadataPayload):
|
||||
kwargs = {}
|
||||
|
||||
if (channel_id := data.get("channel_id")) is not None:
|
||||
kwargs["channel_id"] = int(channel_id)
|
||||
|
||||
if (duration_seconds := data.get("duration_seconds")) is not None:
|
||||
# might need an explicit int cast
|
||||
kwargs["timeout_duration"] = timedelta(seconds=duration_seconds)
|
||||
|
||||
if (custom_message := data.get("custom_message")) is not None:
|
||||
kwargs["custom_message"] = custom_message
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_attrs = (
|
||||
"channel_id",
|
||||
"timeout_duration",
|
||||
"custom_message",
|
||||
)
|
||||
inner = []
|
||||
|
||||
for attr in repr_attrs:
|
||||
if (value := getattr(self, attr)) is not MISSING:
|
||||
inner.append(f"{attr}={value}")
|
||||
inner = " ".join(inner)
|
||||
|
||||
return f"<AutoModActionMetadata {inner}>"
|
||||
|
||||
|
||||
class AutoModAction:
|
||||
"""Represents an action for a guild's auto moderation rule.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
type: :class:`AutoModActionType`
|
||||
The action's type.
|
||||
metadata: :class:`AutoModActionMetadata`
|
||||
The action's metadata.
|
||||
"""
|
||||
|
||||
# note that AutoModActionType.timeout is only valid for trigger type 1?
|
||||
|
||||
__slots__ = (
|
||||
"type",
|
||||
"metadata",
|
||||
)
|
||||
|
||||
def __init__(self, action_type: AutoModActionType, metadata: AutoModActionMetadata):
|
||||
self.type: AutoModActionType = action_type
|
||||
self.metadata: AutoModActionMetadata = metadata
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"type": self.type.value,
|
||||
"metadata": self.metadata.to_dict(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: AutoModActionPayload):
|
||||
return cls(
|
||||
try_enum(AutoModActionType, data["type"]),
|
||||
AutoModActionMetadata.from_dict(data["metadata"]),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AutoModAction type={self.type}>"
|
||||
|
||||
|
||||
class AutoModTriggerMetadata:
|
||||
r"""Represents a rule's trigger metadata, defining additional data used to determine when a rule triggers.
|
||||
|
||||
Depending on the trigger type, different metadata attributes will be used:
|
||||
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
| Attribute | Trigger Types |
|
||||
+=============================+================================================================================+
|
||||
| :attr:`keyword_filter` | :attr:`AutoModTriggerType.keyword` |
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
| :attr:`regex_patterns` | :attr:`AutoModTriggerType.keyword` |
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
| :attr:`presets` | :attr:`AutoModTriggerType.keyword_preset` |
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
| :attr:`allow_list` | :attr:`AutoModTriggerType.keyword`\, :attr:`AutoModTriggerType.keyword_preset` |
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
| :attr:`mention_total_limit` | :attr:`AutoModTriggerType.mention_spam` |
|
||||
+-----------------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
Each attribute has limits that may change based on the trigger type.
|
||||
See `here <https://docs.discord.com/developers/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata-field-limits>`__
|
||||
for information on attribute limits.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
keyword_filter: List[:class:`str`]
|
||||
A list of substrings to filter.
|
||||
|
||||
regex_patterns: List[:class:`str`]
|
||||
A list of regex patterns to filter using Rust-flavored regex, which is not
|
||||
fully compatible with regex syntax supported by the builtin `re` module.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
presets: List[:class:`AutoModKeywordPresetType`]
|
||||
A list of preset keyword sets to filter.
|
||||
|
||||
allow_list: List[:class:`str`]
|
||||
A list of substrings to allow, overriding keyword and regex matches.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
mention_total_limit: :class:`int`
|
||||
The total number of unique role and user mentions allowed.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"keyword_filter",
|
||||
"regex_patterns",
|
||||
"presets",
|
||||
"allow_list",
|
||||
"mention_total_limit",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
keyword_filter: list[str] = MISSING,
|
||||
regex_patterns: list[str] = MISSING,
|
||||
presets: list[AutoModKeywordPresetType] = MISSING,
|
||||
allow_list: list[str] = MISSING,
|
||||
mention_total_limit: int = MISSING,
|
||||
):
|
||||
self.keyword_filter = keyword_filter
|
||||
self.regex_patterns = regex_patterns
|
||||
self.presets = presets
|
||||
self.allow_list = allow_list
|
||||
self.mention_total_limit = mention_total_limit
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
data = {}
|
||||
|
||||
if self.keyword_filter is not MISSING:
|
||||
data["keyword_filter"] = self.keyword_filter
|
||||
|
||||
if self.regex_patterns is not MISSING:
|
||||
data["regex_patterns"] = self.regex_patterns
|
||||
|
||||
if self.presets is not MISSING:
|
||||
data["presets"] = [wordset.value for wordset in self.presets]
|
||||
|
||||
if self.allow_list is not MISSING:
|
||||
data["allow_list"] = self.allow_list
|
||||
|
||||
if self.mention_total_limit is not MISSING:
|
||||
data["mention_total_limit"] = self.mention_total_limit
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: AutoModTriggerMetadataPayload):
|
||||
kwargs = {}
|
||||
|
||||
if (keyword_filter := data.get("keyword_filter")) is not None:
|
||||
kwargs["keyword_filter"] = keyword_filter
|
||||
|
||||
if (regex_patterns := data.get("regex_patterns")) is not None:
|
||||
kwargs["regex_patterns"] = regex_patterns
|
||||
|
||||
if (presets := data.get("presets")) is not None:
|
||||
kwargs["presets"] = [
|
||||
try_enum(AutoModKeywordPresetType, wordset) for wordset in presets
|
||||
]
|
||||
|
||||
if (allow_list := data.get("allow_list")) is not None:
|
||||
kwargs["allow_list"] = allow_list
|
||||
|
||||
if (mention_total_limit := data.get("mention_total_limit")) is not None:
|
||||
kwargs["mention_total_limit"] = mention_total_limit
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_attrs = (
|
||||
"keyword_filter",
|
||||
"regex_patterns",
|
||||
"presets",
|
||||
"allow_list",
|
||||
"mention_total_limit",
|
||||
)
|
||||
inner = []
|
||||
|
||||
for attr in repr_attrs:
|
||||
if (value := getattr(self, attr)) is not MISSING:
|
||||
inner.append(f"{attr}={value}")
|
||||
inner = " ".join(inner)
|
||||
|
||||
return f"<AutoModTriggerMetadata {inner}>"
|
||||
|
||||
|
||||
class AutoModRule(Hashable):
|
||||
"""Represents a guild's auto moderation rule.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two rules are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two rules are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the rule's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the rule's name.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The rule's ID.
|
||||
name: :class:`str`
|
||||
The rule's name.
|
||||
creator_id: :class:`int`
|
||||
The ID of the user who created this rule.
|
||||
event_type: :class:`AutoModEventType`
|
||||
Indicates in what context the rule is checked.
|
||||
trigger_type: :class:`AutoModTriggerType`
|
||||
Indicates what type of information is checked to determine whether the rule is triggered.
|
||||
trigger_metadata: :class:`AutoModTriggerMetadata`
|
||||
The rule's trigger metadata.
|
||||
actions: List[:class:`AutoModAction`]
|
||||
The actions to perform when the rule is triggered.
|
||||
enabled: :class:`bool`
|
||||
Whether this rule is enabled.
|
||||
exempt_role_ids: List[:class:`int`]
|
||||
The IDs of the roles that are exempt from this rule.
|
||||
exempt_channel_ids: List[:class:`int`]
|
||||
The IDs of the channels that are exempt from this rule.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"__dict__",
|
||||
"_state",
|
||||
"id",
|
||||
"guild_id",
|
||||
"name",
|
||||
"creator_id",
|
||||
"event_type",
|
||||
"trigger_type",
|
||||
"trigger_metadata",
|
||||
"actions",
|
||||
"enabled",
|
||||
"exempt_role_ids",
|
||||
"exempt_channel_ids",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
state: ConnectionState,
|
||||
data: AutoModRulePayload,
|
||||
):
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data["id"])
|
||||
self.guild_id: int = int(data["guild_id"])
|
||||
self.name: str = data["name"]
|
||||
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.trigger_metadata: AutoModTriggerMetadata = (
|
||||
AutoModTriggerMetadata.from_dict(data["trigger_metadata"])
|
||||
)
|
||||
self.actions: list[AutoModAction] = [
|
||||
AutoModAction.from_dict(d) for d in data["actions"]
|
||||
]
|
||||
self.enabled: bool = data["enabled"]
|
||||
self.exempt_role_ids: list[int] = [int(r) for r in data["exempt_roles"]]
|
||||
self.exempt_channel_ids: list[int] = [int(c) for c in data["exempt_channels"]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AutoModRule name={self.name} id={self.id}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild | None:
|
||||
"""The guild this rule belongs to."""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def creator(self) -> Member | None:
|
||||
"""The member who created this rule."""
|
||||
if self.guild is None:
|
||||
return None
|
||||
return self.guild.get_member(self.creator_id)
|
||||
|
||||
@cached_property
|
||||
def exempt_roles(self) -> list[Role | Object]:
|
||||
"""The roles that are exempt
|
||||
from this rule.
|
||||
|
||||
If a role is not found in the guild's cache,
|
||||
then it will be returned as an :class:`Object`.
|
||||
"""
|
||||
if self.guild is None:
|
||||
return [Object(role_id) for role_id in self.exempt_role_ids]
|
||||
return [
|
||||
self.guild.get_role(role_id) or Object(role_id)
|
||||
for role_id in self.exempt_role_ids
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def exempt_channels(
|
||||
self,
|
||||
) -> list[TextChannel | ForumChannel | VoiceChannel | Object]:
|
||||
"""The channels that are exempt from this rule.
|
||||
|
||||
If a channel is not found in the guild's cache,
|
||||
then it will be returned as an :class:`Object`.
|
||||
"""
|
||||
if self.guild is None:
|
||||
return [Object(channel_id) for channel_id in self.exempt_channel_ids]
|
||||
return [
|
||||
self.guild.get_channel(channel_id) or Object(channel_id)
|
||||
for channel_id in self.exempt_channel_ids
|
||||
]
|
||||
|
||||
async def delete(self, reason: str | None = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes this rule.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for deleting this rule. Shows up in the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have the Manage Guild permission.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
"""
|
||||
await self._state.http.delete_auto_moderation_rule(
|
||||
self.guild_id, self.id, reason=reason
|
||||
)
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
event_type: AutoModEventType = MISSING,
|
||||
trigger_metadata: AutoModTriggerMetadata = MISSING,
|
||||
actions: list[AutoModAction] = MISSING,
|
||||
enabled: bool = MISSING,
|
||||
exempt_roles: list[Snowflake] = MISSING,
|
||||
exempt_channels: list[Snowflake] = MISSING,
|
||||
reason: str | None = None,
|
||||
) -> AutoModRule | None:
|
||||
"""|coro|
|
||||
|
||||
Edits this rule.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: :class:`str`
|
||||
The rule's new name.
|
||||
event_type: :class:`AutoModEventType`
|
||||
The new context in which the rule is checked.
|
||||
trigger_metadata: :class:`AutoModTriggerMetadata`
|
||||
The new trigger metadata.
|
||||
actions: List[:class:`AutoModAction`]
|
||||
The new actions to perform when the rule is triggered.
|
||||
enabled: :class:`bool`
|
||||
Whether this rule is enabled.
|
||||
exempt_roles: List[:class:`abc.Snowflake`]
|
||||
The roles that will be exempt from this rule.
|
||||
exempt_channels: List[:class:`abc.Snowflake`]
|
||||
The channels that will be exempt from this rule.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this rule. Shows up in the audit log.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`.AutoModRule`]
|
||||
The newly updated rule, if applicable. This is only returned
|
||||
when fields are updated.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have the Manage Guild permission.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
"""
|
||||
http = self._state.http
|
||||
payload = {}
|
||||
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
|
||||
if event_type is not MISSING:
|
||||
payload["event_type"] = event_type.value
|
||||
|
||||
if trigger_metadata is not MISSING:
|
||||
payload["trigger_metadata"] = trigger_metadata.to_dict()
|
||||
|
||||
if actions is not MISSING:
|
||||
payload["actions"] = [a.to_dict() for a in actions]
|
||||
|
||||
if enabled is not MISSING:
|
||||
payload["enabled"] = enabled
|
||||
|
||||
# Maybe consider enforcing limits on the number of exempt roles/channels?
|
||||
if exempt_roles is not MISSING:
|
||||
payload["exempt_roles"] = [r.id for r in exempt_roles]
|
||||
|
||||
if exempt_channels is not MISSING:
|
||||
payload["exempt_channels"] = [c.id for c in exempt_channels]
|
||||
|
||||
if payload:
|
||||
data = await http.edit_auto_moderation_rule(
|
||||
self.guild_id, self.id, payload, reason=reason
|
||||
)
|
||||
return AutoModRule(state=self._state, data=data)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import Callable, Generic, Literal, TypeVar, 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):
|
||||
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[..., int | float] = rand.randrange if integral else rand.uniform # type: ignore
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[False]]) -> float: ...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[True]]) -> int: ...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[bool]) -> int | float: ...
|
||||
|
||||
def delay(self) -> 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)
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .asset import Asset
|
||||
from .types.collectibles import Collectibles as CollectiblesPayload
|
||||
from .types.collectibles import Nameplate as NameplatePayload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import ConnectionState
|
||||
|
||||
__all__ = (
|
||||
"Collectibles",
|
||||
"Nameplate",
|
||||
)
|
||||
|
||||
|
||||
class Collectibles:
|
||||
"""
|
||||
Represents a user or member's equipped collectibles.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two sets of collectibles are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two sets of collectibles are not equal.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
nameplate: :class:`Nameplate`
|
||||
The user's nameplate.
|
||||
"""
|
||||
|
||||
def __init__(self, data: CollectiblesPayload, state: "ConnectionState") -> None:
|
||||
if nameplate_data := data.get("nameplate"):
|
||||
self.nameplate = Nameplate(data=nameplate_data, state=state)
|
||||
else:
|
||||
self.nameplate = None
|
||||
self._state = state
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Collectibles nameplate={self.nameplate}>"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, Collectibles) and self.nameplate == other.nameplate
|
||||
|
||||
|
||||
class Nameplate:
|
||||
"""
|
||||
Represents a Discord Nameplate.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
.. versionchanged:: 2.8
|
||||
Nameplates are now comparable.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two nameplates are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two nameplates are not equal.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
sku_id: :class:`int`
|
||||
The SKU ID of the nameplate.
|
||||
palette: :class:`str`
|
||||
The color palette of the nameplate.
|
||||
"""
|
||||
|
||||
def __init__(self, data: NameplatePayload, state: "ConnectionState") -> None:
|
||||
self.sku_id: int = data["sku_id"]
|
||||
self.palette: str = data["palette"]
|
||||
self._label: str = data["label"]
|
||||
self._asset: str = data["asset"]
|
||||
self._state: "ConnectionState" = state
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Nameplate sku_id={self.sku_id} palette={self.palette}>"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, Nameplate)
|
||||
and self.sku_id == other.sku_id
|
||||
and self.palette == other.palette
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def static_asset(self) -> Asset:
|
||||
"""
|
||||
The static :class:`Asset` of this nameplate.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return Asset._from_collectible(self._state, self._asset)
|
||||
|
||||
@cached_property
|
||||
def animated_asset(self) -> Asset:
|
||||
"""
|
||||
The animated :class:`Asset` of this nameplate.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return Asset._from_collectible(self._state, self._asset, animated=True)
|
||||
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import colorsys
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self, deprecated, override
|
||||
|
||||
__all__ = (
|
||||
"Colour",
|
||||
"Color",
|
||||
)
|
||||
|
||||
|
||||
class Colour:
|
||||
"""Represents a Colour. This class is similar
|
||||
to a (red, green, blue) :class:`tuple`.
|
||||
|
||||
There is an alias for this called Color.
|
||||
|
||||
.. container:: 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):
|
||||
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
|
||||
|
||||
@override
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Colour) and self.value == other.value
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"#{self.value:0>6x}"
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"<Colour value={self.value}>"
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
@property
|
||||
def r(self) -> int:
|
||||
"""Returns the red component of the colour."""
|
||||
return self._get_byte(2)
|
||||
|
||||
@property
|
||||
def g(self) -> int:
|
||||
"""Returns the green component of the colour."""
|
||||
return self._get_byte(1)
|
||||
|
||||
@property
|
||||
def b(self) -> int:
|
||||
"""Returns the blue component of the colour."""
|
||||
return self._get_byte(0)
|
||||
|
||||
def to_rgb(self) -> tuple[int, int, int]:
|
||||
"""Returns an (r, g, b) tuple representing the colour."""
|
||||
return self.r, self.g, self.b
|
||||
|
||||
@classmethod
|
||||
def resolve_value(cls, value: int | Colour | None) -> Self:
|
||||
if value is None or isinstance(value, Colour):
|
||||
return value
|
||||
elif isinstance(value, int):
|
||||
return cls(value=value)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Expected discord.Colour, int, or None but received"
|
||||
f" {value.__class__.__name__} instead."
|
||||
)
|
||||
|
||||
@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: int | str | float | bytes | bytearray | None = 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)
|
||||
|
||||
@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 light_theme(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xfbfbfb``.
|
||||
This will appear transparent on Discord's light theme.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
return cls(0xFBFBFB)
|
||||
|
||||
@classmethod
|
||||
def ash_theme(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x323339``.
|
||||
This will appear transparent on Discord's ash theme.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
return cls(0x323339)
|
||||
|
||||
@classmethod
|
||||
def dark_theme(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1a1a1e``.
|
||||
This will appear transparent on Discord's dark theme.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. versionchanged:: 2.8
|
||||
Updated to match Discord's new theme colour.
|
||||
"""
|
||||
return cls(0x1A1A1E)
|
||||
|
||||
@classmethod
|
||||
def onyx_theme(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x070709``.
|
||||
This will appear transparent on Discord's onyx theme.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
"""
|
||||
return cls(0x070709)
|
||||
|
||||
@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 nitro_pink(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf47fff``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xF47FFF)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"Colour.embed_background is deprecated since version 2.8 and will be removed in version 3.0. This is not relevant anymore since Discord provides the custom themes feature."
|
||||
)
|
||||
def embed_background(cls, theme: str = "dark") -> Self:
|
||||
"""A factory method that returns a :class:`Colour` corresponding to the
|
||||
embed colours on discord clients, with a value of:
|
||||
|
||||
- ``0x2B2D31`` (dark)
|
||||
- ``0xEEEFF1`` (light)
|
||||
- ``0x000000`` (amoled).
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
theme: :class:`str`
|
||||
The theme colour to apply, must be one of "dark", "light", or "amoled".
|
||||
"""
|
||||
themes_cls = {
|
||||
"dark": 0x2B2D31,
|
||||
"light": 0xEEEFF1,
|
||||
"amoled": 0x000000,
|
||||
}
|
||||
|
||||
if theme not in themes_cls:
|
||||
raise TypeError('Theme must be "dark", "light", or "amoled".')
|
||||
|
||||
return cls(themes_cls[theme])
|
||||
|
||||
|
||||
Color = Colour
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .context import *
|
||||
from .core import *
|
||||
from .options import *
|
||||
from .permissions import *
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,476 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||
|
||||
import discord.abc
|
||||
from discord.interactions import Interaction, InteractionMessage, InteractionResponse
|
||||
from discord.webhook.async_ import Webhook
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
import discord
|
||||
|
||||
from .. import AllowedMentions, Bot
|
||||
from ..client import ClientUser
|
||||
from ..cog import Cog
|
||||
from ..embeds import Embed
|
||||
from ..file import File
|
||||
from ..guild import Guild
|
||||
from ..interactions import InteractionChannel
|
||||
from ..member import Member
|
||||
from ..message import Message
|
||||
from ..permissions import Permissions
|
||||
from ..poll import Poll
|
||||
from ..state import ConnectionState
|
||||
from ..ui import BaseView
|
||||
from ..user import User
|
||||
from ..voice import VoiceClient
|
||||
from ..webhook import WebhookMessage
|
||||
from .core import ApplicationCommand, Option
|
||||
|
||||
T = TypeVar("T")
|
||||
CogT = TypeVar("CogT", bound="Cog")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
P = ParamSpec("P")
|
||||
else:
|
||||
P = TypeVar("P")
|
||||
|
||||
__all__ = ("ApplicationContext", "AutocompleteContext")
|
||||
|
||||
|
||||
class ApplicationContext(discord.abc.Messageable):
|
||||
"""Represents a Discord application command interaction context.
|
||||
|
||||
This class is not created manually and is instead passed to application
|
||||
commands as the first parameter.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
bot: :class:`.Bot`
|
||||
The bot that the command belongs to.
|
||||
interaction: :class:`.Interaction`
|
||||
The interaction object that invoked the command.
|
||||
"""
|
||||
|
||||
def __init__(self, bot: Bot, interaction: Interaction):
|
||||
self.bot = bot
|
||||
self.interaction = interaction
|
||||
|
||||
# below attributes will be set after initialization
|
||||
self.focused: Option = None # type: ignore
|
||||
self.value: str = None # type: ignore
|
||||
self.options: dict = None # type: ignore
|
||||
|
||||
self._state: ConnectionState = self.interaction._state
|
||||
|
||||
async def _get_channel(self) -> InteractionChannel | None:
|
||||
return self.interaction.channel
|
||||
|
||||
async def invoke(
|
||||
self,
|
||||
command: ApplicationCommand[CogT, P, T],
|
||||
/,
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> T:
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
This is useful if you want to just call the callback that a
|
||||
:class:`.ApplicationCommand` holds internally.
|
||||
|
||||
.. note::
|
||||
|
||||
This does not handle converters, checks, cooldowns, pre-invoke,
|
||||
or after-invoke hooks in any matter. It calls the internal callback
|
||||
directly as-if it was a regular function.
|
||||
You must take care in passing the proper arguments when
|
||||
using this function.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`.ApplicationCommand`
|
||||
The command that is going to be called.
|
||||
\*args
|
||||
The arguments to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The command argument to invoke is missing.
|
||||
"""
|
||||
return await command(self, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def command(self) -> ApplicationCommand | None:
|
||||
"""The command that this context belongs to."""
|
||||
return self.interaction.command
|
||||
|
||||
@command.setter
|
||||
def command(self, value: ApplicationCommand | None) -> None:
|
||||
self.interaction.command = value
|
||||
|
||||
@property
|
||||
def channel(self) -> InteractionChannel | None:
|
||||
"""Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]:
|
||||
Returns the channel associated with this context's command. Shorthand for :attr:`.Interaction.channel`.
|
||||
"""
|
||||
return self.interaction.channel
|
||||
|
||||
@property
|
||||
def channel_id(self) -> int | None:
|
||||
"""Returns the ID of the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.channel_id`.
|
||||
"""
|
||||
return self.interaction.channel_id
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild | None:
|
||||
"""Returns the guild associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.guild`.
|
||||
"""
|
||||
return self.interaction.guild
|
||||
|
||||
@property
|
||||
def guild_id(self) -> int | None:
|
||||
"""Returns the ID of the guild associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.guild_id`.
|
||||
"""
|
||||
return self.interaction.guild_id
|
||||
|
||||
@property
|
||||
def locale(self) -> str | None:
|
||||
"""Returns the locale of the guild associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.locale`.
|
||||
"""
|
||||
return self.interaction.locale
|
||||
|
||||
@property
|
||||
def guild_locale(self) -> str | None:
|
||||
"""Returns the locale of the guild associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.guild_locale`.
|
||||
"""
|
||||
return self.interaction.guild_locale
|
||||
|
||||
@property
|
||||
def app_permissions(self) -> Permissions:
|
||||
return self.interaction.app_permissions
|
||||
|
||||
@property
|
||||
def me(self) -> Member | ClientUser | None:
|
||||
"""Union[:class:`.Member`, :class:`.ClientUser`]:
|
||||
Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message
|
||||
message contexts, or when :meth:`Intents.guilds` is absent.
|
||||
"""
|
||||
return (
|
||||
self.interaction.guild.me
|
||||
if self.interaction.guild is not None
|
||||
else self.bot.user
|
||||
)
|
||||
|
||||
@property
|
||||
def message(self) -> Message | None:
|
||||
"""Returns the message sent with this context's command.
|
||||
Shorthand for :attr:`.Interaction.message`, if applicable.
|
||||
"""
|
||||
return self.interaction.message
|
||||
|
||||
@property
|
||||
def user(self) -> Member | User:
|
||||
"""Returns the user that sent this context's command.
|
||||
Shorthand for :attr:`.Interaction.user`.
|
||||
"""
|
||||
return self.interaction.user # type: ignore # command user will never be None
|
||||
|
||||
author: Member | User = user
|
||||
|
||||
@property
|
||||
def voice_client(self) -> VoiceClient | None:
|
||||
"""Returns the voice client associated with this context's command.
|
||||
Shorthand for :attr:`Interaction.guild.voice_client<~discord.Guild.voice_client>`, if applicable.
|
||||
"""
|
||||
if self.interaction.guild is None:
|
||||
return None
|
||||
|
||||
return self.interaction.guild.voice_client
|
||||
|
||||
@property
|
||||
def response(self) -> InteractionResponse:
|
||||
"""Returns the response object associated with this context's command.
|
||||
Shorthand for :attr:`.Interaction.response`.
|
||||
"""
|
||||
return self.interaction.response
|
||||
|
||||
@property
|
||||
def selected_options(self) -> list[dict[str, Any]] | None:
|
||||
"""The options and values that were selected by the user when sending the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[List[Dict[:class:`str`, Any]]]
|
||||
A dictionary containing the options and values that were selected by the user when the command
|
||||
was processed, if applicable. Returns ``None`` if the command has not yet been invoked,
|
||||
or if there are no options defined for that command.
|
||||
"""
|
||||
return self.interaction.data.get("options", None)
|
||||
|
||||
@property
|
||||
def unselected_options(self) -> list[Option] | None:
|
||||
"""The options that were not provided by the user when sending the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[List[:class:`.Option`]]
|
||||
A list of Option objects (if any) that were not selected by the user when the command was processed.
|
||||
Returns ``None`` if there are no options defined for that command.
|
||||
"""
|
||||
if self.command.options is not None: # type: ignore
|
||||
if self.selected_options:
|
||||
return [
|
||||
option
|
||||
for option in self.command.options # type: ignore
|
||||
if option.to_dict()["name"]
|
||||
not in [opt["name"] for opt in self.selected_options]
|
||||
]
|
||||
else:
|
||||
return self.command.options # type: ignore
|
||||
return None
|
||||
|
||||
@property
|
||||
def attachment_size_limit(self) -> int:
|
||||
"""Returns the attachment size limit associated with this context's interaction.
|
||||
Shorthand for :attr:`.Interaction.attachment_size_limit`.
|
||||
"""
|
||||
return self.interaction.attachment_size_limit
|
||||
|
||||
@property
|
||||
@discord.utils.copy_doc(InteractionResponse.send_modal)
|
||||
def send_modal(self) -> Callable[..., Awaitable[Interaction]]:
|
||||
return self.interaction.response.send_modal
|
||||
|
||||
@overload
|
||||
async def respond(
|
||||
self,
|
||||
content: Any | None = None,
|
||||
embed: Embed | None = None,
|
||||
view: BaseView | None = None,
|
||||
tts: bool = False,
|
||||
ephemeral: bool = False,
|
||||
allowed_mentions: AllowedMentions | None = None,
|
||||
file: File | None = None,
|
||||
files: list[File] | None = None,
|
||||
poll: Poll | None = None,
|
||||
delete_after: float | None = None,
|
||||
silent: bool = False,
|
||||
suppress_embeds: bool = False,
|
||||
) -> Interaction | WebhookMessage: ...
|
||||
|
||||
@overload
|
||||
async def respond(
|
||||
self,
|
||||
content: Any | None = None,
|
||||
embeds: list[Embed] | None = None,
|
||||
view: BaseView | None = None,
|
||||
tts: bool = False,
|
||||
ephemeral: bool = False,
|
||||
allowed_mentions: AllowedMentions | None = None,
|
||||
file: File | None = None,
|
||||
files: list[File] | None = None,
|
||||
poll: Poll | None = None,
|
||||
delete_after: float | None = None,
|
||||
silent: bool = False,
|
||||
suppress_embeds: bool = False,
|
||||
) -> Interaction | WebhookMessage: ...
|
||||
|
||||
@discord.utils.copy_doc(Interaction.respond)
|
||||
async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage:
|
||||
return await self.interaction.respond(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@discord.utils.copy_doc(InteractionResponse.send_message)
|
||||
def send_response(self) -> Callable[..., Awaitable[Interaction]]:
|
||||
if not self.interaction.response.is_done():
|
||||
return self.interaction.response.send_message
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Interaction was already issued a response. Try using"
|
||||
f" {type(self).__name__}.send_followup() instead."
|
||||
)
|
||||
|
||||
@property
|
||||
@discord.utils.copy_doc(Webhook.send)
|
||||
def send_followup(self) -> Callable[..., Awaitable[WebhookMessage]]:
|
||||
if self.interaction.response.is_done():
|
||||
return self.followup.send
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Interaction was not yet issued a response. Try using"
|
||||
f" {type(self).__name__}.respond() first."
|
||||
)
|
||||
|
||||
@property
|
||||
@discord.utils.copy_doc(InteractionResponse.defer)
|
||||
def defer(self) -> Callable[..., Awaitable[None]]:
|
||||
return self.interaction.response.defer
|
||||
|
||||
@property
|
||||
def followup(self) -> Webhook:
|
||||
"""Returns the followup webhook for followup interactions."""
|
||||
return self.interaction.followup
|
||||
|
||||
async def delete(self, *, delay: float | None = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the original interaction response message.
|
||||
|
||||
This is a higher level interface to :meth:`Interaction.delete_original_response`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
delay: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait before deleting the message.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Deleting the message failed.
|
||||
Forbidden
|
||||
You do not have proper permissions to delete the message.
|
||||
"""
|
||||
if not self.interaction.response.is_done():
|
||||
await self.defer()
|
||||
|
||||
return await self.interaction.delete_original_response(delay=delay)
|
||||
|
||||
@property
|
||||
@discord.utils.copy_doc(Interaction.edit_original_response)
|
||||
def edit(self) -> Callable[..., Awaitable[InteractionMessage]]:
|
||||
return self.interaction.edit_original_response
|
||||
|
||||
@property
|
||||
def cog(self) -> Cog | None:
|
||||
"""Returns the cog associated with this context's command.
|
||||
``None`` if it does not exist.
|
||||
"""
|
||||
if self.command is None:
|
||||
return None
|
||||
|
||||
return self.command.cog
|
||||
|
||||
def is_guild_authorised(self) -> bool:
|
||||
""":class:`bool`: Checks if the invoked command is guild-installed.
|
||||
This is a shortcut for :meth:`Interaction.is_guild_authorised`.
|
||||
|
||||
There is an alias for this called :meth:`.is_guild_authorized`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return self.interaction.is_guild_authorised()
|
||||
|
||||
def is_user_authorised(self) -> bool:
|
||||
""":class:`bool`: Checks if the invoked command is user-installed.
|
||||
This is a shortcut for :meth:`Interaction.is_user_authorised`.
|
||||
|
||||
There is an alias for this called :meth:`.is_user_authorized`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return self.interaction.is_user_authorised()
|
||||
|
||||
def is_guild_authorized(self) -> bool:
|
||||
""":class:`bool`: An alias for :meth:`.is_guild_authorised`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return self.is_guild_authorised()
|
||||
|
||||
def is_user_authorized(self) -> bool:
|
||||
""":class:`bool`: An alias for :meth:`.is_user_authorised`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
return self.is_user_authorised()
|
||||
|
||||
|
||||
class AutocompleteContext:
|
||||
"""Represents context for a slash command's option autocomplete.
|
||||
|
||||
This class is not created manually and is instead passed to an :class:`.Option`'s autocomplete callback.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
bot: :class:`.Bot`
|
||||
The bot that the command belongs to.
|
||||
interaction: :class:`.Interaction`
|
||||
The interaction object that invoked the autocomplete.
|
||||
focused: :class:`.Option`
|
||||
The option the user is currently typing.
|
||||
value: :class:`.str`
|
||||
The content of the focused option.
|
||||
options: Dict[:class:`str`, Any]
|
||||
A name to value mapping of the options that the user has selected before this option.
|
||||
"""
|
||||
|
||||
__slots__ = ("bot", "interaction", "focused", "value", "options")
|
||||
|
||||
def __init__(self, bot: Bot, interaction: Interaction):
|
||||
self.bot = bot
|
||||
self.interaction = interaction
|
||||
|
||||
self.focused: Option = None # type: ignore
|
||||
self.value: str = None # type: ignore
|
||||
self.options: dict = None # type: ignore
|
||||
|
||||
@property
|
||||
def cog(self) -> Cog | None:
|
||||
"""Returns the cog associated with this context's command.
|
||||
``None`` if it does not exist.
|
||||
"""
|
||||
if self.command is None:
|
||||
return None
|
||||
|
||||
return self.command.cog
|
||||
|
||||
@property
|
||||
def command(self) -> ApplicationCommand | None:
|
||||
"""The command that this context belongs to."""
|
||||
return self.interaction.command
|
||||
|
||||
@command.setter
|
||||
def command(self, value: ApplicationCommand | None) -> None:
|
||||
self.interaction.command = value
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,557 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import TypeAliasType
|
||||
else:
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
from ..abc import GuildChannel, Mentionable
|
||||
from ..channel import (
|
||||
CategoryChannel,
|
||||
DMChannel,
|
||||
ForumChannel,
|
||||
MediaChannel,
|
||||
StageChannel,
|
||||
TextChannel,
|
||||
Thread,
|
||||
VoiceChannel,
|
||||
)
|
||||
from ..commands import ApplicationContext, AutocompleteContext
|
||||
from ..enums import ChannelType
|
||||
from ..enums import Enum as DiscordEnum
|
||||
from ..enums import SlashCommandOptionType
|
||||
from ..utils import MISSING, basic_autocomplete
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..cog import Cog
|
||||
from ..ext.commands import Converter
|
||||
from ..member import Member
|
||||
from ..message import Attachment
|
||||
from ..role import Role
|
||||
from ..user import User
|
||||
|
||||
InputType = Union[
|
||||
Type[str],
|
||||
Type[bool],
|
||||
Type[int],
|
||||
Type[float],
|
||||
Type[GuildChannel],
|
||||
Type[Thread],
|
||||
Type[Member],
|
||||
Type[User],
|
||||
Type[Attachment],
|
||||
Type[Role],
|
||||
Type[Mentionable],
|
||||
SlashCommandOptionType,
|
||||
Converter,
|
||||
Type[Converter],
|
||||
Type[Enum],
|
||||
Type[DiscordEnum],
|
||||
]
|
||||
|
||||
AutocompleteReturnType = Union[
|
||||
Iterable["OptionChoice"], Iterable[str], Iterable[int], Iterable[float]
|
||||
]
|
||||
T = TypeVar("T", bound=AutocompleteReturnType)
|
||||
MaybeAwaitable = Union[T, Awaitable[T]]
|
||||
AutocompleteFunction = Union[
|
||||
Callable[[AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
|
||||
Callable[[Cog, AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
|
||||
Callable[
|
||||
[AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
|
||||
MaybeAwaitable[AutocompleteReturnType],
|
||||
],
|
||||
Callable[
|
||||
[Cog, AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
|
||||
MaybeAwaitable[AutocompleteReturnType],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ThreadOption",
|
||||
"Option",
|
||||
"OptionChoice",
|
||||
"option",
|
||||
)
|
||||
|
||||
CHANNEL_TYPE_MAP = {
|
||||
TextChannel: ChannelType.text,
|
||||
VoiceChannel: ChannelType.voice,
|
||||
StageChannel: ChannelType.stage_voice,
|
||||
CategoryChannel: ChannelType.category,
|
||||
Thread: ChannelType.public_thread,
|
||||
ForumChannel: ChannelType.forum,
|
||||
MediaChannel: ChannelType.media,
|
||||
DMChannel: ChannelType.private,
|
||||
}
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThreadOption:
|
||||
"""Represents a class that can be passed as the ``input_type`` for an :class:`Option` class.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
thread_type: Literal["public", "private", "news"]
|
||||
The thread type to expect for this options input.
|
||||
"""
|
||||
|
||||
def __init__(self, thread_type: Literal["public", "private", "news"]):
|
||||
type_map = {
|
||||
"public": ChannelType.public_thread,
|
||||
"private": ChannelType.private_thread,
|
||||
"news": ChannelType.news_thread,
|
||||
}
|
||||
self._type = type_map[thread_type]
|
||||
|
||||
|
||||
class Option:
|
||||
"""Represents a selectable option for a slash command.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
input_type: Union[Type[:class:`str`], Type[:class:`bool`], Type[:class:`int`], Type[:class:`float`], Type[:class:`.abc.GuildChannel`], Type[:class:`Thread`], Type[:class:`Member`], Type[:class:`User`], Type[:class:`Attachment`], Type[:class:`Role`], Type[:class:`.abc.Mentionable`], :class:`SlashCommandOptionType`, Type[:class:`.ext.commands.Converter`], Type[:class:`enums.Enum`], Type[:class:`Enum`]]
|
||||
The type of input that is expected for this option. This can be a :class:`SlashCommandOptionType`,
|
||||
an associated class, a channel type, a :class:`Converter`, a converter class or an :class:`enum.Enum`.
|
||||
If a :class:`enum.Enum` is used and it has up to 25 values, :attr:`choices` will be automatically filled. If the :class:`enum.Enum` has more than 25 values, :attr:`autocomplete` will be implemented with :func:`discord.utils.basic_autocomplete` instead.
|
||||
name: :class:`str`
|
||||
The name of this option visible in the UI.
|
||||
Inherits from the variable name if not provided as a parameter.
|
||||
description: Optional[:class:`str`]
|
||||
The description of this option.
|
||||
Must be 100 characters or fewer. If :attr:`input_type` is a :class:`enum.Enum` and :attr:`description` is not specified, :attr:`input_type`'s docstring will be used.
|
||||
choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]]
|
||||
The list of available choices for this option.
|
||||
Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair).
|
||||
If provided, the input from the user must match one of the choices in the list.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether this option is required.
|
||||
default: Optional[:class:`Any`]
|
||||
The default value for this option. If provided, ``required`` will be considered ``False``.
|
||||
min_value: Optional[:class:`int`]
|
||||
The minimum value that can be entered.
|
||||
Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`.
|
||||
max_value: Optional[:class:`int`]
|
||||
The maximum value that can be entered.
|
||||
Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`.
|
||||
min_length: Optional[:class:`int`]
|
||||
The minimum length of the string that can be entered. Must be between 0 and 6000 (inclusive).
|
||||
Only applies to Options with an :attr:`input_type` of :class:`str`.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive).
|
||||
Only applies to Options with an :attr:`input_type` of :class:`str`.
|
||||
channel_types: list[:class:`discord.ChannelType`] | None
|
||||
A list of channel types that can be selected in this option.
|
||||
Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`.
|
||||
If this argument is used, :attr:`input_type` will be ignored.
|
||||
name_localizations: Dict[:class:`str`, :class:`str`]
|
||||
The name localizations for this option. The values of this should be ``"locale": "name"``.
|
||||
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
|
||||
description_localizations: Dict[:class:`str`, :class:`str`]
|
||||
The description localizations for this option. The values of this should be ``"locale": "description"``.
|
||||
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Basic usage: ::
|
||||
|
||||
@bot.slash_command(guild_ids=[...])
|
||||
async def hello(
|
||||
ctx: discord.ApplicationContext,
|
||||
name: Option(str, "Enter your name"),
|
||||
age: Option(int, "Enter your age", min_value=1, max_value=99, default=18)
|
||||
# passing the default value makes an argument optional
|
||||
# you also can create optional argument using:
|
||||
# age: Option(int, "Enter your age") = 18
|
||||
):
|
||||
await ctx.respond(f"Hello! Your name is {name} and you are {age} years old.")
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
input_type: SlashCommandOptionType
|
||||
converter: Converter | type[Converter] | None = None
|
||||
|
||||
def __init__(
|
||||
self, input_type: InputType = str, /, description: str | None = None, **kwargs
|
||||
) -> None:
|
||||
self.name: str | None = kwargs.pop("name", None)
|
||||
if self.name is not None:
|
||||
self.name = str(self.name)
|
||||
self._parameter_name = self.name # default
|
||||
input_type = self._parse_type_alias(input_type)
|
||||
input_type = self._strip_none_type(input_type)
|
||||
self._raw_type: InputType | tuple = input_type
|
||||
|
||||
enum_choices = []
|
||||
input_type_is_class = isinstance(input_type, type)
|
||||
if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)):
|
||||
if description is None and input_type.__doc__ is not None:
|
||||
description = inspect.cleandoc(input_type.__doc__)
|
||||
if description and len(description) > 100:
|
||||
description = description[:97] + "..."
|
||||
_log.warning(
|
||||
"Option %s's description was truncated due to Enum %s's docstring exceeding 100 characters.",
|
||||
self.name,
|
||||
input_type,
|
||||
)
|
||||
enum_choices = [OptionChoice(e.name, e.value) for e in input_type]
|
||||
value_class = enum_choices[0].value.__class__
|
||||
if value_class in SlashCommandOptionType.__members__ and all(
|
||||
isinstance(elem.value, value_class) for elem in enum_choices
|
||||
):
|
||||
input_type = SlashCommandOptionType.from_datatype(
|
||||
enum_choices[0].value.__class__
|
||||
)
|
||||
else:
|
||||
enum_choices = [OptionChoice(e.name, str(e.value)) for e in input_type]
|
||||
input_type = SlashCommandOptionType.string
|
||||
|
||||
self.description = description or "No description provided"
|
||||
self.channel_types: list[ChannelType] = kwargs.pop("channel_types", [])
|
||||
|
||||
if self.channel_types:
|
||||
self.input_type = SlashCommandOptionType.channel
|
||||
elif isinstance(input_type, SlashCommandOptionType):
|
||||
self.input_type = input_type
|
||||
else:
|
||||
from ..ext.commands import Converter
|
||||
|
||||
if isinstance(input_type, tuple) and any(
|
||||
issubclass(op, ApplicationContext) for op in input_type
|
||||
):
|
||||
input_type = next(
|
||||
op for op in input_type if issubclass(op, ApplicationContext)
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(input_type, Converter)
|
||||
or input_type_is_class
|
||||
and issubclass(input_type, Converter)
|
||||
):
|
||||
self.converter = input_type
|
||||
self._raw_type = str
|
||||
self.input_type = SlashCommandOptionType.string
|
||||
else:
|
||||
try:
|
||||
self.input_type = SlashCommandOptionType.from_datatype(input_type)
|
||||
except TypeError as exc:
|
||||
from ..ext.commands.converter import CONVERTER_MAPPING
|
||||
|
||||
if input_type not in CONVERTER_MAPPING:
|
||||
raise exc
|
||||
self.converter = CONVERTER_MAPPING[input_type]
|
||||
self._raw_type = str
|
||||
self.input_type = SlashCommandOptionType.string
|
||||
else:
|
||||
if self.input_type == SlashCommandOptionType.channel:
|
||||
if not isinstance(self._raw_type, tuple):
|
||||
if hasattr(input_type, "__args__"):
|
||||
self._raw_type = input_type.__args__ # type: ignore # Union.__args__
|
||||
else:
|
||||
self._raw_type = (input_type,)
|
||||
if not self.channel_types:
|
||||
self.channel_types = [
|
||||
CHANNEL_TYPE_MAP[t]
|
||||
for t in self._raw_type
|
||||
if t is not GuildChannel
|
||||
]
|
||||
self.required: bool = (
|
||||
kwargs.pop("required", True) if "default" not in kwargs else False
|
||||
)
|
||||
self.default = kwargs.pop("default", None)
|
||||
|
||||
self._autocomplete: AutocompleteFunction | None = None
|
||||
self._autocomplete_is_instance_method: bool = False
|
||||
self.autocomplete = kwargs.pop("autocomplete", None)
|
||||
if len(enum_choices) > 25:
|
||||
self.choices: list[OptionChoice] = []
|
||||
for e in enum_choices:
|
||||
e.value = str(e.value)
|
||||
self.autocomplete = basic_autocomplete(enum_choices)
|
||||
self.input_type = SlashCommandOptionType.string
|
||||
else:
|
||||
self.choices: list[OptionChoice] = enum_choices or [
|
||||
o if isinstance(o, OptionChoice) else OptionChoice(o)
|
||||
for o in kwargs.pop("choices", [])
|
||||
]
|
||||
|
||||
if self.input_type == SlashCommandOptionType.integer:
|
||||
minmax_types = (int, type(None))
|
||||
minmax_typehint = Optional[int]
|
||||
elif self.input_type == SlashCommandOptionType.number:
|
||||
minmax_types = (int, float, type(None))
|
||||
minmax_typehint = Optional[Union[int, float]]
|
||||
else:
|
||||
minmax_types = (type(None),)
|
||||
minmax_typehint = type(None)
|
||||
|
||||
if self.input_type == SlashCommandOptionType.string:
|
||||
minmax_length_types = (int, type(None))
|
||||
minmax_length_typehint = Optional[int]
|
||||
else:
|
||||
minmax_length_types = (type(None),)
|
||||
minmax_length_typehint = type(None)
|
||||
|
||||
self.min_value: int | float | None = kwargs.pop("min_value", None)
|
||||
self.max_value: int | float | None = kwargs.pop("max_value", None)
|
||||
self.min_length: int | None = kwargs.pop("min_length", None)
|
||||
self.max_length: int | None = kwargs.pop("max_length", None)
|
||||
|
||||
if (
|
||||
self.input_type != SlashCommandOptionType.integer
|
||||
and self.input_type != SlashCommandOptionType.number
|
||||
and (self.min_value or self.max_value)
|
||||
):
|
||||
raise AttributeError(
|
||||
"Option does not take min_value or max_value if not of type "
|
||||
"SlashCommandOptionType.integer or SlashCommandOptionType.number"
|
||||
)
|
||||
if self.input_type != SlashCommandOptionType.string and (
|
||||
self.min_length or self.max_length
|
||||
):
|
||||
raise AttributeError(
|
||||
"Option does not take min_length or max_length if not of type str"
|
||||
)
|
||||
|
||||
if self.min_value is not None and not isinstance(self.min_value, minmax_types):
|
||||
raise TypeError(
|
||||
f"Expected {minmax_typehint} for min_value, got"
|
||||
f' "{type(self.min_value).__name__}"'
|
||||
)
|
||||
if self.max_value is not None and not isinstance(self.max_value, minmax_types):
|
||||
raise TypeError(
|
||||
f"Expected {minmax_typehint} for max_value, got"
|
||||
f' "{type(self.max_value).__name__}"'
|
||||
)
|
||||
|
||||
if self.min_length is not None:
|
||||
if not isinstance(self.min_length, minmax_length_types):
|
||||
raise TypeError(
|
||||
f"Expected {minmax_length_typehint} for min_length,"
|
||||
f' got "{type(self.min_length).__name__}"'
|
||||
)
|
||||
if self.min_length < 0 or self.min_length > 6000:
|
||||
raise AttributeError(
|
||||
"min_length must be between 0 and 6000 (inclusive)"
|
||||
)
|
||||
if self.max_length is not None:
|
||||
if not isinstance(self.max_length, minmax_length_types):
|
||||
raise TypeError(
|
||||
f"Expected {minmax_length_typehint} for max_length,"
|
||||
f' got "{type(self.max_length).__name__}"'
|
||||
)
|
||||
if self.max_length < 1 or self.max_length > 6000:
|
||||
raise AttributeError("max_length must between 1 and 6000 (inclusive)")
|
||||
|
||||
self.name_localizations = kwargs.pop("name_localizations", MISSING)
|
||||
self.description_localizations = kwargs.pop(
|
||||
"description_localizations", MISSING
|
||||
)
|
||||
|
||||
if input_type is None:
|
||||
raise TypeError("input_type cannot be NoneType.")
|
||||
|
||||
@staticmethod
|
||||
def _parse_type_alias(input_type: InputType) -> InputType:
|
||||
if isinstance(input_type, TypeAliasType):
|
||||
return input_type.__value__
|
||||
return input_type
|
||||
|
||||
@staticmethod
|
||||
def _strip_none_type(input_type):
|
||||
if isinstance(input_type, SlashCommandOptionType):
|
||||
return input_type
|
||||
|
||||
if input_type is type(None):
|
||||
raise TypeError("Option type cannot be only NoneType")
|
||||
|
||||
args = ()
|
||||
if isinstance(input_type, types.UnionType):
|
||||
args = get_args(input_type)
|
||||
elif getattr(input_type, "__origin__", None) is Union:
|
||||
args = get_args(input_type)
|
||||
elif isinstance(input_type, tuple):
|
||||
args = input_type
|
||||
|
||||
if args:
|
||||
filtered = tuple(t for t in args if t is not type(None))
|
||||
if not filtered:
|
||||
raise TypeError("Option type cannot be only NoneType")
|
||||
if len(filtered) == 1:
|
||||
return filtered[0]
|
||||
|
||||
return filtered
|
||||
|
||||
return input_type
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
as_dict = {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"type": self.input_type.value,
|
||||
"required": self.required,
|
||||
"choices": [c.to_dict() for c in self.choices],
|
||||
"autocomplete": bool(self.autocomplete),
|
||||
}
|
||||
if self.name_localizations is not MISSING:
|
||||
as_dict["name_localizations"] = self.name_localizations
|
||||
if self.description_localizations is not MISSING:
|
||||
as_dict["description_localizations"] = self.description_localizations
|
||||
if self.channel_types:
|
||||
as_dict["channel_types"] = [t.value for t in self.channel_types]
|
||||
if self.min_value is not None:
|
||||
as_dict["min_value"] = self.min_value
|
||||
if self.max_value is not None:
|
||||
as_dict["max_value"] = self.max_value
|
||||
if self.min_length is not None:
|
||||
as_dict["min_length"] = self.min_length
|
||||
if self.max_length is not None:
|
||||
as_dict["max_length"] = self.max_length
|
||||
|
||||
return as_dict
|
||||
|
||||
def __repr__(self):
|
||||
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
|
||||
|
||||
@property
|
||||
def autocomplete(self) -> AutocompleteFunction | None:
|
||||
"""
|
||||
The autocomplete handler for the option. Accepts a callable (sync or async)
|
||||
that takes a single required argument of :class:`AutocompleteContext` or two arguments
|
||||
of :class:`discord.Cog` (being the command's cog) and :class:`AutocompleteContext`.
|
||||
The callable must return an iterable of :class:`str` or :class:`OptionChoice`.
|
||||
Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[AutocompleteFunction]
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
.. note::
|
||||
Does not validate the input value against the autocomplete results.
|
||||
"""
|
||||
return self._autocomplete
|
||||
|
||||
@autocomplete.setter
|
||||
def autocomplete(self, value: AutocompleteFunction | None) -> None:
|
||||
self._autocomplete = value
|
||||
# this is done here so it does not have to be computed every time the autocomplete is invoked
|
||||
if self._autocomplete is not None:
|
||||
self._autocomplete_is_instance_method = (
|
||||
sum(
|
||||
1
|
||||
for param in inspect.signature(
|
||||
self._autocomplete
|
||||
).parameters.values()
|
||||
if param.default == param.empty
|
||||
and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
|
||||
class OptionChoice:
|
||||
"""
|
||||
Represents a name:value pairing for a selected :class:`.Option`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The name of the choice. Shown in the UI when selecting an option.
|
||||
value: Optional[Union[:class:`str`, :class:`int`, :class:`float`]]
|
||||
The value of the choice. If not provided, will use the value of ``name``.
|
||||
name_localizations: Dict[:class:`str`, :class:`str`]
|
||||
The name localizations for this choice. The values of this should be ``"locale": "name"``.
|
||||
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
value: str | int | float | None = None,
|
||||
name_localizations: dict[str, str] = MISSING,
|
||||
):
|
||||
self.name = str(name)
|
||||
self.value = value if value is not None else name
|
||||
self.name_localizations = name_localizations
|
||||
|
||||
def to_dict(self) -> dict[str, str | int | float]:
|
||||
as_dict = {"name": self.name, "value": self.value}
|
||||
if self.name_localizations is not MISSING:
|
||||
as_dict["name_localizations"] = self.name_localizations
|
||||
|
||||
return as_dict
|
||||
|
||||
|
||||
def option(name, input_type=None, **kwargs):
|
||||
"""A decorator that can be used instead of typehinting :class:`.Option`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
parameter_name: :class:`str`
|
||||
The name of the target function parameter this option is mapped to.
|
||||
This allows you to have a separate UI ``name`` and parameter name.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
resolved_name = kwargs.pop("parameter_name", None) or name
|
||||
itype = (
|
||||
kwargs.pop("type", None)
|
||||
or input_type
|
||||
or func.__annotations__.get(resolved_name, str)
|
||||
)
|
||||
func.__annotations__[resolved_name] = Option(itype, name=name, **kwargs)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from ..enums import InteractionContextType
|
||||
from ..permissions import Permissions
|
||||
from .core import ApplicationCommand
|
||||
|
||||
__all__ = ("default_permissions", "guild_only", "is_nsfw")
|
||||
|
||||
|
||||
def default_permissions(**perms: bool) -> Callable:
|
||||
"""A decorator that limits the usage of an application command to members with certain
|
||||
permissions.
|
||||
|
||||
The permissions passed in must be exactly like the properties shown under
|
||||
:class:`.discord.Permissions`.
|
||||
|
||||
.. note::
|
||||
These permissions can be updated by server administrators per-guild. As such, these are only "defaults", as the
|
||||
name suggests. If you want to make sure that a user **always** has the specified permissions regardless, you
|
||||
should use an internal check such as :func:`~.ext.commands.has_permissions`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**perms: Dict[:class:`str`, :class:`bool`]
|
||||
An argument list of permissions to check for.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from discord import default_permissions
|
||||
|
||||
@bot.slash_command()
|
||||
@default_permissions(manage_messages=True)
|
||||
async def test(ctx):
|
||||
await ctx.respond('You can manage messages.')
|
||||
"""
|
||||
|
||||
invalid = set(perms) - set(Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def inner(command: Callable):
|
||||
if isinstance(command, ApplicationCommand):
|
||||
if command.parent is not None:
|
||||
raise RuntimeError(
|
||||
"Permission restrictions can only be set on top-level commands"
|
||||
)
|
||||
command.default_member_permissions = Permissions(**perms)
|
||||
else:
|
||||
command.__default_member_permissions__ = Permissions(**perms)
|
||||
return command
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def guild_only() -> Callable:
|
||||
"""A decorator that limits the usage of an application command to guild contexts.
|
||||
The command won't be able to be used in private message channels.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from discord import guild_only
|
||||
|
||||
@bot.slash_command()
|
||||
@guild_only()
|
||||
async def test(ctx):
|
||||
await ctx.respond("You're in a guild.")
|
||||
"""
|
||||
|
||||
def inner(command: Callable):
|
||||
if isinstance(command, ApplicationCommand):
|
||||
command.contexts = {InteractionContextType.guild}
|
||||
else:
|
||||
command.__contexts__ = {InteractionContextType.guild}
|
||||
|
||||
return command
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def is_nsfw() -> Callable:
|
||||
"""A decorator that limits the usage of an application command to 18+ channels and users.
|
||||
In guilds, the command will only be able to be used in channels marked as NSFW.
|
||||
In DMs, users must have opted into age-restricted commands via privacy settings.
|
||||
|
||||
Note that apps intending to be listed in the App Directory cannot have NSFW commands.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from discord import is_nsfw
|
||||
|
||||
@bot.slash_command()
|
||||
@is_nsfw()
|
||||
async def test(ctx):
|
||||
await ctx.respond("This command is age restricted.")
|
||||
"""
|
||||
|
||||
def inner(command: Callable):
|
||||
if isinstance(command, ApplicationCommand):
|
||||
command.nsfw = True
|
||||
else:
|
||||
command.__nsfw__ = True
|
||||
|
||||
return command
|
||||
|
||||
return inner
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from .abc import Messageable
|
||||
|
||||
TypingT = TypeVar("TypingT", bound="Typing")
|
||||
|
||||
__all__ = ("Typing",)
|
||||
|
||||
|
||||
def _typing_done_callback(fut: asyncio.Task) -> None:
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
fut.exception()
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Typing:
|
||||
def __init__(self, messageable: Messageable) -> None:
|
||||
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
|
||||
self.messageable: Messageable = 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: TypingT) -> TypingT:
|
||||
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: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.task.cancel()
|
||||
|
||||
async def __aenter__(self: TypingT) -> TypingT:
|
||||
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: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.task.cancel()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator, Literal
|
||||
|
||||
from .asset import Asset, AssetMixin
|
||||
from .partial_emoji import PartialEmoji, _EmojiTag
|
||||
from .user import User
|
||||
from .utils import MISSING, SnowflakeList, snowflake_time
|
||||
|
||||
__all__ = (
|
||||
"Emoji",
|
||||
"GuildEmoji",
|
||||
"AppEmoji",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from .abc import Snowflake
|
||||
from .guild import Guild
|
||||
from .role import Role
|
||||
from .state import ConnectionState
|
||||
from .types.emoji import Emoji as EmojiPayload
|
||||
|
||||
|
||||
class BaseEmoji(_EmojiTag, AssetMixin):
|
||||
__slots__: tuple[str, ...] = (
|
||||
"require_colons",
|
||||
"animated",
|
||||
"managed",
|
||||
"id",
|
||||
"name",
|
||||
"_state",
|
||||
"user",
|
||||
"available",
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: EmojiPayload):
|
||||
self._state: ConnectionState = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, emoji: EmojiPayload):
|
||||
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)
|
||||
user = emoji.get("user")
|
||||
self.user: User | None = 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"<BaseEmoji id={self.id} name={self.name!r} animated={self.animated}>"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, _EmojiTag) and self.id == other.id
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self.id >> 22
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime:
|
||||
"""Returns the emoji's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Returns the URL of the emoji."""
|
||||
url = f"{Asset.BASE}/emojis/{self.id}.{self.extension}"
|
||||
if self.animated:
|
||||
url += "?animated=true"
|
||||
return url
|
||||
|
||||
@property
|
||||
def mention(self) -> str:
|
||||
"""Return a string that allows you to mention the emoji in a message."""
|
||||
if self.animated:
|
||||
return f"<a:{self.name}:{self.id}>"
|
||||
return f"<:{self.name}:{self.id}>"
|
||||
|
||||
@property
|
||||
def extension(self) -> Literal["webp", "png"]:
|
||||
"""Return the file extension of the emoji.
|
||||
|
||||
.. versionadded:: 2.7.1
|
||||
"""
|
||||
return "webp" if self.animated else "png"
|
||||
|
||||
|
||||
class GuildEmoji(BaseEmoji):
|
||||
"""Represents a custom emoji in a guild.
|
||||
|
||||
Depending on the way this object was created, some attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: 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 name of the emoji.
|
||||
id: :class:`int`
|
||||
The emoji's ID.
|
||||
require_colons: :class:`bool`
|
||||
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
|
||||
animated: :class:`bool`
|
||||
Whether an emoji is animated or not.
|
||||
managed: :class:`bool`
|
||||
If this 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 the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
|
||||
having the :attr:`~Permissions.manage_emojis` permission.
|
||||
"""
|
||||
|
||||
__slots__: tuple[str, ...] = (
|
||||
"_roles",
|
||||
"guild_id",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload):
|
||||
self.guild_id: int = guild.id
|
||||
self._roles: SnowflakeList = SnowflakeList(map(int, data.get("roles", [])))
|
||||
super().__init__(state=state, data=data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"<GuildEmoji"
|
||||
f" id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def roles(self) -> list[Role]:
|
||||
"""A :class:`list` of roles that is allowed to use this emoji.
|
||||
|
||||
If roles is empty, the emoji is unrestricted.
|
||||
"""
|
||||
guild = self.guild
|
||||
if guild is None:
|
||||
return []
|
||||
|
||||
return [role for role in guild.roles if self._roles.has(role.id)]
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild:
|
||||
"""The guild this emoji belongs to."""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
def is_usable(self) -> bool:
|
||||
"""Whether the bot can use this emoji.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
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: str | None = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` 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 emojis.
|
||||
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: str | None = None,
|
||||
) -> GuildEmoji:
|
||||
r"""|coro|
|
||||
|
||||
Edits the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` 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:`~discord.abc.Snowflake`]]
|
||||
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this emoji. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to edit emojis.
|
||||
HTTPException
|
||||
An error occurred editing the emoji.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`GuildEmoji`
|
||||
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 GuildEmoji(guild=self.guild, data=data, state=self._state)
|
||||
|
||||
|
||||
Emoji = GuildEmoji
|
||||
|
||||
|
||||
class AppEmoji(BaseEmoji):
|
||||
"""Represents a custom emoji from an application.
|
||||
|
||||
Depending on the way this object was created, some attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
.. container:: 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 name of the emoji.
|
||||
id: :class:`int`
|
||||
The emoji's ID.
|
||||
require_colons: :class:`bool`
|
||||
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
|
||||
animated: :class:`bool`
|
||||
Whether an emoji is animated or not.
|
||||
managed: :class:`bool`
|
||||
If this emoji is managed by a Twitch integration.
|
||||
application_id: Optional[:class:`int`]
|
||||
The application ID the emoji belongs to, if available.
|
||||
available: :class:`bool`
|
||||
Whether the emoji is available for use.
|
||||
user: Optional[:class:`User`]
|
||||
The user that created the emoji.
|
||||
"""
|
||||
|
||||
__slots__: tuple[str, ...] = ("application_id",)
|
||||
|
||||
def __init__(
|
||||
self, *, application_id: int, state: ConnectionState, data: EmojiPayload
|
||||
):
|
||||
self.application_id: int = application_id
|
||||
super().__init__(state=state, data=data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AppEmoji id={self.id} name={self.name!r} animated={self.animated}>"
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild:
|
||||
"""The guild this emoji belongs to. This is always `None` for :class:`AppEmoji`."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def roles(self) -> list[Role]:
|
||||
"""A :class:`list` of roles that is allowed to use this emoji. This is always empty for :class:`AppEmoji`."""
|
||||
return []
|
||||
|
||||
def is_usable(self) -> bool:
|
||||
"""Whether the bot can use this emoji."""
|
||||
return self.application_id == self._state.application_id
|
||||
|
||||
async def delete(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the application emoji.
|
||||
|
||||
You must own the emoji to do this.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You are not allowed to delete the emoji.
|
||||
HTTPException
|
||||
An error occurred deleting the emoji.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_application_emoji(self.application_id, self.id)
|
||||
if self._state.cache_app_emojis and self._state.get_emoji(self.id):
|
||||
self._state._remove_emoji(self)
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
) -> AppEmoji:
|
||||
r"""|coro|
|
||||
|
||||
Edits the application emoji.
|
||||
|
||||
You must own the emoji to do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The new emoji name.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to edit the emoji.
|
||||
HTTPException
|
||||
An error occurred editing the emoji.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`AppEmoji`
|
||||
The newly updated emoji.
|
||||
"""
|
||||
|
||||
payload = {}
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
|
||||
data = await self._state.http.edit_application_emoji(
|
||||
self.application_id, self.id, payload=payload
|
||||
)
|
||||
return self._state.maybe_store_app_emoji(self.application_id, data)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohttp import ClientResponse, ClientWebSocketResponse
|
||||
|
||||
try:
|
||||
from requests import Response
|
||||
|
||||
_ResponseType = Union[ClientResponse, Response]
|
||||
except ModuleNotFoundError:
|
||||
_ResponseType = ClientResponse
|
||||
|
||||
from .interactions import Interaction
|
||||
|
||||
__all__ = (
|
||||
"DiscordException",
|
||||
"ClientException",
|
||||
"NoMoreItems",
|
||||
"GatewayNotFound",
|
||||
"ValidationError",
|
||||
"HTTPException",
|
||||
"Forbidden",
|
||||
"NotFound",
|
||||
"DiscordServerError",
|
||||
"InvalidData",
|
||||
"InvalidArgument",
|
||||
"LoginFailure",
|
||||
"ConnectionClosed",
|
||||
"PrivilegedIntentsRequired",
|
||||
"InteractionResponded",
|
||||
"ExtensionError",
|
||||
"ExtensionAlreadyLoaded",
|
||||
"ExtensionNotLoaded",
|
||||
"NoEntryPointError",
|
||||
"ExtensionFailed",
|
||||
"ExtensionNotFound",
|
||||
"ApplicationCommandError",
|
||||
"CheckFailure",
|
||||
"ApplicationCommandInvokeError",
|
||||
"MissingVoiceDependenciesError",
|
||||
)
|
||||
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for pycord
|
||||
|
||||
Ideally speaking, this could be caught to handle any exceptions raised from this library.
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class NoMoreItems(DiscordException):
|
||||
"""Exception that is raised when an async iteration operation has no more items."""
|
||||
|
||||
|
||||
class GatewayNotFound(DiscordException):
|
||||
"""An exception that is raised when the gateway for Discord could not be found"""
|
||||
|
||||
def __init__(self):
|
||||
message = "The gateway to connect to discord was not found."
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ValidationError(DiscordException):
|
||||
"""An Exception that is raised when there is a Validation Error."""
|
||||
|
||||
|
||||
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: 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("In %s: %s" % t for t in errors.items())
|
||||
self.text = f"{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`
|
||||
"""
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""Exception that's raised for when status code 404 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
|
||||
|
||||
class DiscordServerError(HTTPException):
|
||||
"""Exception that's raised for when a 500 range status code occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
|
||||
|
||||
class InvalidData(ClientException):
|
||||
"""Exception that's raised when the library encounters unknown
|
||||
or invalid data from Discord.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidArgument(ClientException):
|
||||
"""Exception that's raised when an argument to a function
|
||||
is invalid some way (e.g. wrong value or wrong type).
|
||||
|
||||
This could be considered the parallel of ``ValueError`` and
|
||||
``TypeError`` except inherited from :exc:`ClientException` and thus
|
||||
:exc:`DiscordException`.
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
socket: ClientWebSocketResponse,
|
||||
*,
|
||||
shard_id: int | None,
|
||||
code: int | None = 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.shard_id: int | None = shard_id
|
||||
super().__init__(f"Shard ID {self.shard_id} WebSocket closed with {self.code}")
|
||||
|
||||
|
||||
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: int | None):
|
||||
self.shard_id: int | None = shard_id
|
||||
msg = (
|
||||
"Shard ID %s 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 % shard_id)
|
||||
|
||||
|
||||
class InteractionResponded(ClientException):
|
||||
"""Exception that's raised when sending another interaction response using
|
||||
:class:`InteractionResponse` when one has already been done before.
|
||||
|
||||
An interaction can only respond once.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
interaction: :class:`Interaction`
|
||||
The interaction that's already been responded to.
|
||||
"""
|
||||
|
||||
def __init__(self, interaction: Interaction):
|
||||
self.interaction: Interaction = interaction
|
||||
super().__init__("This interaction has already been responded to before")
|
||||
|
||||
|
||||
class ExtensionError(DiscordException):
|
||||
"""Base exception for extension related errors.
|
||||
|
||||
This inherits from :exc:`~discord.DiscordException`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The extension that had an error.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str | None = None, *args: Any, name: str) -> None:
|
||||
self.name: str = name
|
||||
message = message or f"Extension {name!r} had an error."
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace("@everyone", "@\u200beveryone").replace(
|
||||
"@here", "@\u200bhere"
|
||||
)
|
||||
super().__init__(m, *args)
|
||||
|
||||
|
||||
class ExtensionAlreadyLoaded(ExtensionError):
|
||||
"""An exception raised when an extension has already been loaded.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(f"Extension {name!r} is already loaded.", name=name)
|
||||
|
||||
|
||||
class ExtensionNotLoaded(ExtensionError):
|
||||
"""An exception raised when an extension was not loaded.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(f"Extension {name!r} has not been loaded.", name=name)
|
||||
|
||||
|
||||
class NoEntryPointError(ExtensionError):
|
||||
"""An exception raised when an extension does not have a ``setup`` entry point function.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(f"Extension {name!r} has no 'setup' function.", name=name)
|
||||
|
||||
|
||||
class ExtensionFailed(ExtensionError):
|
||||
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The extension that had the error.
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, original: Exception) -> None:
|
||||
self.original: Exception = original
|
||||
msg = (
|
||||
f"Extension {name!r} raised an error: {original.__class__.__name__}:"
|
||||
f" {original}"
|
||||
)
|
||||
super().__init__(msg, name=name)
|
||||
|
||||
|
||||
class ExtensionNotFound(ExtensionError):
|
||||
"""An exception raised when an extension is not found.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
|
||||
.. versionchanged:: 1.3
|
||||
Made the ``original`` attribute always None.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The extension that had the error.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
msg = f"Extension {name!r} could not be found."
|
||||
super().__init__(msg, name=name)
|
||||
|
||||
|
||||
class ApplicationCommandError(DiscordException):
|
||||
r"""The base exception type for all application command related errors.
|
||||
|
||||
This inherits from :exc:`DiscordException`.
|
||||
|
||||
This exception and exceptions inherited from it are handled
|
||||
in a special way as they are caught and passed into a special event
|
||||
from :class:`.Bot`\, :func:`.on_command_error`.
|
||||
"""
|
||||
|
||||
|
||||
class CheckFailure(ApplicationCommandError):
|
||||
"""Exception raised when the predicates in :attr:`.Command.checks` have failed.
|
||||
|
||||
This inherits from :exc:`ApplicationCommandError`
|
||||
"""
|
||||
|
||||
|
||||
class ApplicationCommandInvokeError(ApplicationCommandError):
|
||||
"""Exception raised when the command being invoked raised an exception.
|
||||
|
||||
This inherits from :exc:`ApplicationCommandError`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, e: Exception) -> None:
|
||||
self.original: Exception = e
|
||||
super().__init__(
|
||||
f"Application Command raised an exception: {e.__class__.__name__}: {e}"
|
||||
)
|
||||
|
||||
|
||||
class MissingVoiceDependenciesError(RuntimeError, DiscordException):
|
||||
"""Raised when required voice dependencies are not installed.
|
||||
|
||||
.. note::
|
||||
This exception inherits from both :exc:`RuntimeError` and :exc:`DiscordException`.
|
||||
|
||||
Attributes:
|
||||
missing: tuple[str, ...]
|
||||
The missing dependencies that are required for voice support.
|
||||
"""
|
||||
|
||||
def __init__(self, missing: tuple[str, ...]) -> None:
|
||||
self.missing: tuple[str, ...] = missing
|
||||
deps = ", ".join(missing)
|
||||
super().__init__(
|
||||
f"{deps} {'is' if len(missing) == 1 else 'are'} required for voice support. "
|
||||
'Install them with "pip install py-cord[voice]".'
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .bot import *
|
||||
from .context import *
|
||||
from .core import *
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user