On branch DiscordProfile

Initial commit
This commit is contained in:
EG
2026-07-01 15:15:07 +03:00
commit d4bf750c9e
3125 changed files with 601334 additions and 0 deletions
@@ -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()
@@ -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)
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 *
@@ -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