Añadiendo todos los archivos del proyecto (incluidos secretos y venv)
This commit is contained in:
98
venv/lib/python3.12/site-packages/discord/__init__.py
Normal file
98
venv/lib/python3.12/site-packages/discord/__init__.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Discord API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A basic wrapper for the Discord API.
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'discord'
|
||||
__author__ = 'Rapptz'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2015-present Rapptz'
|
||||
__version__ = '2.6.4'
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
||||
import logging
|
||||
from typing import NamedTuple, Literal
|
||||
|
||||
from .client import *
|
||||
from .appinfo import *
|
||||
from .user import *
|
||||
from .emoji import *
|
||||
from .partial_emoji import *
|
||||
from .activity import *
|
||||
from .channel import *
|
||||
from .guild import *
|
||||
from .flags import *
|
||||
from .member import *
|
||||
from .message import *
|
||||
from .asset import *
|
||||
from .errors import *
|
||||
from .permissions import *
|
||||
from .role import *
|
||||
from .file import *
|
||||
from .colour import *
|
||||
from .integrations import *
|
||||
from .invite import *
|
||||
from .template import *
|
||||
from .welcome_screen import *
|
||||
from .sku import *
|
||||
from .widget import *
|
||||
from .object import *
|
||||
from .reaction import *
|
||||
from . import (
|
||||
utils as utils,
|
||||
opus as opus,
|
||||
abc as abc,
|
||||
ui as ui,
|
||||
app_commands as app_commands,
|
||||
)
|
||||
from .enums import *
|
||||
from .embeds import *
|
||||
from .mentions import *
|
||||
from .shard import *
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import *
|
||||
from .audit_logs import *
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
from .sticker import *
|
||||
from .stage_instance import *
|
||||
from .scheduled_event import *
|
||||
from .interactions import *
|
||||
from .components import *
|
||||
from .threads import *
|
||||
from .automod import *
|
||||
from .poll import *
|
||||
from .soundboard import *
|
||||
from .subscription import *
|
||||
from .presences import *
|
||||
from .primary_guild import *
|
||||
from .onboarding import *
|
||||
|
||||
|
||||
class VersionInfo(NamedTuple):
|
||||
major: int
|
||||
minor: int
|
||||
micro: int
|
||||
releaselevel: Literal['alpha', 'beta', 'candidate', 'final']
|
||||
serial: int
|
||||
|
||||
|
||||
version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=4, releaselevel='final', serial=0)
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
# This is a backwards compatibility hack and should be removed in v3
|
||||
# Essentially forcing the exception to have different base classes
|
||||
# In the future, this should only inherit from ClientException
|
||||
if len(MissingApplicationID.__bases__) == 1:
|
||||
MissingApplicationID.__bases__ = (app_commands.AppCommandError, ClientException)
|
||||
|
||||
del logging, NamedTuple, Literal, VersionInfo
|
||||
357
venv/lib/python3.12/site-packages/discord/__main__.py
Normal file
357
venv/lib/python3.12/site-packages/discord/__main__.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 Optional, Tuple, Dict
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path, PurePath, PureWindowsPath
|
||||
|
||||
import discord
|
||||
import importlib.metadata
|
||||
import aiohttp
|
||||
import platform
|
||||
|
||||
|
||||
def show_version() -> None:
|
||||
entries = []
|
||||
|
||||
entries.append('- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(sys.version_info))
|
||||
version_info = discord.version_info
|
||||
entries.append('- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(version_info))
|
||||
if version_info.releaselevel != 'final':
|
||||
version = importlib.metadata.version('discord.py')
|
||||
if version:
|
||||
entries.append(f' - discord.py 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: argparse.ArgumentParser, args: argparse.Namespace) -> None:
|
||||
if args.version:
|
||||
show_version()
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
_bot_template = """#!/usr/bin/env python3
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import config
|
||||
|
||||
class Bot(commands.{base}):
|
||||
def __init__(self, intents: discord.Intents, **kwargs):
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), intents=intents, **kwargs)
|
||||
|
||||
async def setup_hook(self):
|
||||
for cog in config.cogs:
|
||||
try:
|
||||
await 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}})')
|
||||
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
bot = Bot(intents=intents)
|
||||
|
||||
# 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}
|
||||
async def setup(bot):
|
||||
await bot.add_cog({name}(bot))
|
||||
'''
|
||||
|
||||
_cog_extras = """
|
||||
async def cog_load(self):
|
||||
# loading logic goes here
|
||||
pass
|
||||
|
||||
async 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_app_command_error(self, interaction, error):
|
||||
# error handling to every application 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: Dict[str, Optional[str]] = {
|
||||
'<': '-',
|
||||
'>': '-',
|
||||
':': '-',
|
||||
'"': '-',
|
||||
# '/': '-', 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: argparse.ArgumentParser, name: str, *, replace_spaces: bool = False) -> Path:
|
||||
if isinstance(name, Path):
|
||||
return name
|
||||
|
||||
if sys.platform == 'win32':
|
||||
forbidden = (
|
||||
'CON',
|
||||
'PRN',
|
||||
'AUX',
|
||||
'NUL',
|
||||
'COM1',
|
||||
'COM2',
|
||||
'COM3',
|
||||
'COM4',
|
||||
'COM5',
|
||||
'COM6',
|
||||
'COM7',
|
||||
'COM8',
|
||||
'COM9',
|
||||
'LPT1',
|
||||
'LPT2',
|
||||
'LPT3',
|
||||
'LPT4',
|
||||
'LPT5',
|
||||
'LPT6',
|
||||
'LPT7',
|
||||
'LPT8',
|
||||
'LPT9',
|
||||
)
|
||||
if len(name) <= 4 and name.upper() in forbidden:
|
||||
parser.error('invalid directory name given, use a different one')
|
||||
path = PurePath(name)
|
||||
if isinstance(path, PureWindowsPath) and path.drive:
|
||||
drive, rest = path.parts[0], path.parts[1:]
|
||||
transformed = tuple(map(lambda p: p.translate(_translation_table), rest))
|
||||
name = drive + '\\'.join(transformed)
|
||||
|
||||
else:
|
||||
name = name.translate(_translation_table)
|
||||
if replace_spaces:
|
||||
name = name.replace(' ', '-')
|
||||
return Path(name)
|
||||
|
||||
|
||||
def newbot(parser: argparse.ArgumentParser, args: argparse.Namespace) -> 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: argparse.ArgumentParser, args: argparse.Namespace) -> 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[argparse.ArgumentParser]) -> 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[argparse.ArgumentParser]) -> 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 discord.py')
|
||||
parser.add_argument('-v', '--version', action='store_true', help='shows the library version')
|
||||
parser.set_defaults(func=core)
|
||||
|
||||
subparser = parser.add_subparsers(dest='subcommand', title='subcommands')
|
||||
add_newbot_args(subparser)
|
||||
add_newcog_args(subparser)
|
||||
return parser, parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser, args = parse_args()
|
||||
args.func(parser, args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
34
venv/lib/python3.12/site-packages/discord/_types.py
Normal file
34
venv/lib/python3.12/site-packages/discord/_types.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 TypeVar, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeVar
|
||||
from .client import Client
|
||||
|
||||
ClientT = TypeVar('ClientT', bound=Client, covariant=True, default=Client)
|
||||
else:
|
||||
ClientT = TypeVar('ClientT', bound='Client', covariant=True)
|
||||
2158
venv/lib/python3.12/site-packages/discord/abc.py
Normal file
2158
venv/lib/python3.12/site-packages/discord/abc.py
Normal file
File diff suppressed because it is too large
Load Diff
900
venv/lib/python3.12/site-packages/discord/activity.py
Normal file
900
venv/lib/python3.12/site-packages/discord/activity.py
Normal file
@@ -0,0 +1,900 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 Any, Dict, List, Optional, TYPE_CHECKING, Union, overload
|
||||
|
||||
from .asset import Asset
|
||||
from .enums import ActivityType, StatusDisplayType, try_enum
|
||||
from .colour import Colour
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .utils import _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
'BaseActivity',
|
||||
'Activity',
|
||||
'Streaming',
|
||||
'Game',
|
||||
'Spotify',
|
||||
'CustomActivity',
|
||||
)
|
||||
|
||||
"""If curious, this is the current schema for an activity.
|
||||
|
||||
It's fairly long so I will document it here:
|
||||
|
||||
All keys are optional.
|
||||
|
||||
state: str (max: 128),
|
||||
details: str (max: 128)
|
||||
timestamps: dict
|
||||
start: int (min: 1)
|
||||
end: int (min: 1)
|
||||
assets: dict
|
||||
large_image: str (max: 32)
|
||||
large_text: str (max: 128)
|
||||
small_image: str (max: 32)
|
||||
small_text: str (max: 128)
|
||||
party: dict
|
||||
id: str (max: 128),
|
||||
size: List[int] (max-length: 2)
|
||||
elem: int (min: 1)
|
||||
secrets: dict
|
||||
match: str (max: 128)
|
||||
join: str (max: 128)
|
||||
spectate: str (max: 128)
|
||||
instance: bool
|
||||
application_id: str
|
||||
name: str (max: 128)
|
||||
url: str
|
||||
type: int
|
||||
sync_id: str
|
||||
session_id: str
|
||||
flags: int
|
||||
buttons: list[str (max: 32)]
|
||||
|
||||
There are also activity flags which are mostly uninteresting for the library atm.
|
||||
|
||||
t.ActivityFlags = {
|
||||
INSTANCE: 1,
|
||||
JOIN: 2,
|
||||
SPECTATE: 4,
|
||||
JOIN_REQUEST: 8,
|
||||
SYNC: 16,
|
||||
PLAY: 32
|
||||
}
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.activity import (
|
||||
Activity as ActivityPayload,
|
||||
ActivityTimestamps,
|
||||
ActivityParty,
|
||||
ActivityAssets,
|
||||
)
|
||||
|
||||
from .state import ConnectionState
|
||||
|
||||
|
||||
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: Any) -> None:
|
||||
self._created_at: Optional[float] = kwargs.pop('created_at', None)
|
||||
|
||||
@property
|
||||
def created_at(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._created_at is not None:
|
||||
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
|
||||
|
||||
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 state. For example, "In Game".
|
||||
details: Optional[:class:`str`]
|
||||
The detail of the user's current activity.
|
||||
platform: Optional[:class:`str`]
|
||||
The user's current platform.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
timestamps: :class:`dict`
|
||||
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: :class:`dict`
|
||||
A dictionary representing the images and their hover text of an activity.
|
||||
It contains the following optional keys:
|
||||
|
||||
- ``large_image``: A string representing the ID for the large image asset.
|
||||
- ``large_text``: A string representing the text when hovering over the large image asset.
|
||||
- ``large_url``: A string representing the URL of 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.
|
||||
- ``small_url``: A string representing the URL of the small image asset.
|
||||
|
||||
party: :class:`dict`
|
||||
A dictionary representing the activity party. It contains the following optional keys:
|
||||
|
||||
- ``id``: A string representing the party ID.
|
||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
||||
buttons: List[:class:`str`]
|
||||
A list of strings representing the labels of custom buttons shown in a rich presence.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
emoji: Optional[:class:`PartialEmoji`]
|
||||
The emoji that belongs to this activity.
|
||||
details_url: Optional[:class:`str`]
|
||||
A URL that is linked to when clicking on the details text of the activity.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
state_url: Optional[:class:`str`]
|
||||
A URL that is linked to when clicking on the state text of the activity.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
status_display_type: Optional[:class:`StatusDisplayType`]
|
||||
Determines which field from the user's status text is displayed
|
||||
in the members list.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'state',
|
||||
'details',
|
||||
'timestamps',
|
||||
'platform',
|
||||
'assets',
|
||||
'party',
|
||||
'flags',
|
||||
'sync_id',
|
||||
'session_id',
|
||||
'type',
|
||||
'name',
|
||||
'url',
|
||||
'application_id',
|
||||
'emoji',
|
||||
'buttons',
|
||||
'state_url',
|
||||
'details_url',
|
||||
'status_display_type',
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.state: Optional[str] = kwargs.pop('state', None)
|
||||
self.details: Optional[str] = kwargs.pop('details', None)
|
||||
self.timestamps: ActivityTimestamps = kwargs.pop('timestamps', {})
|
||||
self.platform: Optional[str] = kwargs.pop('platform', None)
|
||||
self.assets: ActivityAssets = kwargs.pop('assets', {})
|
||||
self.party: ActivityParty = kwargs.pop('party', {})
|
||||
self.application_id: Optional[int] = _get_as_snowflake(kwargs, 'application_id')
|
||||
self.name: Optional[str] = kwargs.pop('name', None)
|
||||
self.url: Optional[str] = kwargs.pop('url', None)
|
||||
self.flags: int = kwargs.pop('flags', 0)
|
||||
self.sync_id: Optional[str] = kwargs.pop('sync_id', None)
|
||||
self.session_id: Optional[str] = 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)
|
||||
)
|
||||
|
||||
emoji = kwargs.pop('emoji', None)
|
||||
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None
|
||||
|
||||
self.state_url: Optional[str] = kwargs.pop('state_url', None)
|
||||
self.details_url: Optional[str] = kwargs.pop('details_url', None)
|
||||
|
||||
status_display_type = kwargs.pop('status_display_type', None)
|
||||
self.status_display_type: Optional[StatusDisplayType] = (
|
||||
status_display_type
|
||||
if isinstance(status_display_type, StatusDisplayType)
|
||||
else try_enum(StatusDisplayType, status_display_type)
|
||||
if status_display_type is not None
|
||||
else None
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = (
|
||||
('type', self.type),
|
||||
('name', self.name),
|
||||
('url', self.url),
|
||||
('platform', self.platform),
|
||||
('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()
|
||||
if self.status_display_type:
|
||||
ret['status_display_type'] = int(self.status_display_type.value)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def start(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps['start'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def end(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps['end'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def large_image_url(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable."""
|
||||
try:
|
||||
large_image = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return self._image_url(large_image)
|
||||
|
||||
@property
|
||||
def small_image_url(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable."""
|
||||
try:
|
||||
small_image = self.assets['small_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return self._image_url(small_image)
|
||||
|
||||
def _image_url(self, image: str) -> Optional[str]:
|
||||
if image.startswith('mp:'):
|
||||
return f'https://media.discordapp.net/{image[3:]}'
|
||||
elif self.application_id is not None:
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{image}.png'
|
||||
|
||||
@property
|
||||
def large_image_text(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: 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) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: 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.
|
||||
platform: Optional[:class:`str`]
|
||||
Where the user is playing from (ie. PS5, Xbox).
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
assets: :class:`dict`
|
||||
A dictionary representing the images and their hover text of a game.
|
||||
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.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
"""
|
||||
|
||||
__slots__ = ('name', '_end', '_start', 'platform', 'assets')
|
||||
|
||||
def __init__(self, name: str, **extra: Any) -> None:
|
||||
super().__init__(**extra)
|
||||
self.name: str = name
|
||||
self.platform: Optional[str] = extra.get('platform')
|
||||
self.assets: ActivityAssets = extra.get('assets', {}) or {}
|
||||
|
||||
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:
|
||||
""":class:`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) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: 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) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: 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} platform={self.platform!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,
|
||||
'platform': str(self.platform) if self.platform else None,
|
||||
'assets': self.assets,
|
||||
}
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, Game) and other.name == self.name
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Streaming(BaseActivity):
|
||||
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
||||
|
||||
This is typically displayed via **Streaming** on the official Discord client.
|
||||
|
||||
.. 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: :class:`dict`
|
||||
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
|
||||
"""
|
||||
|
||||
__slots__ = ('platform', 'name', 'game', 'url', 'details', 'assets')
|
||||
|
||||
def __init__(self, *, name: Optional[str], url: str, **extra: Any) -> None:
|
||||
super().__init__(**extra)
|
||||
self.platform: Optional[str] = name
|
||||
self.name: Optional[str] = extra.pop('details', name)
|
||||
self.game: Optional[str] = extra.pop('state', None)
|
||||
self.url: str = url
|
||||
self.details: Optional[str] = extra.pop('details', self.name) # compatibility
|
||||
self.assets: ActivityAssets = extra.pop('assets', {})
|
||||
|
||||
@property
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.streaming`.
|
||||
"""
|
||||
return ActivityType.streaming
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Streaming name={self.name!r} platform={self.platform!r}>'
|
||||
|
||||
@property
|
||||
def twitch_name(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
|
||||
|
||||
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
|
||||
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
|
||||
"""
|
||||
|
||||
try:
|
||||
name = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
|
||||
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: object) -> bool:
|
||||
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
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: Any) -> None:
|
||||
self._state: str = data.pop('state', '')
|
||||
self._details: str = data.pop('details', '')
|
||||
self._timestamps: ActivityTimestamps = 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: Optional[str] = data.pop('session_id')
|
||||
self._created_at: Optional[float] = data.pop('created_at', None)
|
||||
|
||||
@property
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.listening`.
|
||||
"""
|
||||
return ActivityType.listening
|
||||
|
||||
@property
|
||||
def created_at(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: 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:
|
||||
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :attr:`color`"""
|
||||
return Colour(0x1DB954)
|
||||
|
||||
@property
|
||||
def color(self) -> Colour:
|
||||
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :attr:`colour`"""
|
||||
return self.colour
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'flags': 48, # SYNC | PLAY
|
||||
'name': 'Spotify',
|
||||
'assets': self._assets,
|
||||
'party': self._party,
|
||||
'sync_id': self._sync_id,
|
||||
'session_id': self._session_id,
|
||||
'timestamps': self._timestamps,
|
||||
'details': self._details,
|
||||
'state': self._state,
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
""":class:`str`: The activity's name. This will always return "Spotify"."""
|
||||
return 'Spotify'
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, Spotify)
|
||||
and other._session_id == self._session_id
|
||||
and other._sync_id == self._sync_id
|
||||
and other.start == self.start
|
||||
)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._session_id)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'Spotify'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>'
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
""":class:`str`: The title of the song being played."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def artists(self) -> List[str]:
|
||||
"""List[:class:`str`]: The artists of the song being played."""
|
||||
return self._state.split('; ')
|
||||
|
||||
@property
|
||||
def artist(self) -> str:
|
||||
""":class:`str`: The artist of the song being played.
|
||||
|
||||
This does not attempt to split the artist information into
|
||||
multiple artists. Useful if there's only a single artist.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def album(self) -> str:
|
||||
""":class:`str`: The album that the song being played belongs to."""
|
||||
return self._assets.get('large_text', '')
|
||||
|
||||
@property
|
||||
def album_cover_url(self) -> str:
|
||||
""":class:`str`: The album cover image URL from Spotify's CDN."""
|
||||
large_image = self._assets.get('large_image', '')
|
||||
if large_image[:8] != 'spotify:':
|
||||
return ''
|
||||
album_image_id = large_image[8:]
|
||||
return 'https://i.scdn.co/image/' + album_image_id
|
||||
|
||||
@property
|
||||
def track_id(self) -> str:
|
||||
""":class:`str`: The track ID used by Spotify to identify this song."""
|
||||
return self._sync_id
|
||||
|
||||
@property
|
||||
def track_url(self) -> str:
|
||||
""":class:`str`: The track URL to listen on Spotify.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return f'https://open.spotify.com/track/{self.track_id}'
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
|
||||
# the start key will be present here
|
||||
return datetime.datetime.fromtimestamp(self._timestamps['start'] / 1000, tz=datetime.timezone.utc) # type: ignore
|
||||
|
||||
@property
|
||||
def end(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
|
||||
# the end key will be present here
|
||||
return datetime.datetime.fromtimestamp(self._timestamps['end'] / 1000, tz=datetime.timezone.utc) # type: ignore
|
||||
|
||||
@property
|
||||
def duration(self) -> datetime.timedelta:
|
||||
""":class:`datetime.timedelta`: The duration of the song being played."""
|
||||
return self.end - self.start
|
||||
|
||||
@property
|
||||
def party_id(self) -> str:
|
||||
""":class:`str`: The party ID of the listening party."""
|
||||
return self._party.get('id', '')
|
||||
|
||||
|
||||
class CustomActivity(BaseActivity):
|
||||
"""Represents a custom activity from Discord.
|
||||
|
||||
.. 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.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'emoji', 'state')
|
||||
|
||||
def __init__(
|
||||
self, name: Optional[str], *, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any
|
||||
) -> None:
|
||||
super().__init__(**extra)
|
||||
self.name: Optional[str] = name
|
||||
self.state: Optional[str] = extra.pop('state', name)
|
||||
if self.name == 'Custom Status':
|
||||
self.name = self.state
|
||||
|
||||
self.emoji: Optional[PartialEmoji]
|
||||
if emoji is None:
|
||||
self.emoji = emoji
|
||||
elif isinstance(emoji, dict):
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
elif isinstance(emoji, str):
|
||||
self.emoji = PartialEmoji(name=emoji)
|
||||
elif isinstance(emoji, PartialEmoji):
|
||||
self.emoji = emoji
|
||||
else:
|
||||
raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
|
||||
|
||||
@property
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.custom`.
|
||||
"""
|
||||
return ActivityType.custom
|
||||
|
||||
def to_dict(self) -> 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: object) -> bool:
|
||||
return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, str(self.emoji)))
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.emoji:
|
||||
if self.name:
|
||||
return f'{self.emoji} {self.name}'
|
||||
return str(self.emoji)
|
||||
else:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
|
||||
|
||||
|
||||
ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify]
|
||||
|
||||
|
||||
@overload
|
||||
def create_activity(data: ActivityPayload, state: ConnectionState) -> ActivityTypes: ...
|
||||
|
||||
|
||||
@overload
|
||||
def create_activity(data: None, state: ConnectionState) -> None: ...
|
||||
|
||||
|
||||
def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> Optional[ActivityTypes]:
|
||||
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') # type: ignore
|
||||
except KeyError:
|
||||
ret = Activity(**data)
|
||||
else:
|
||||
# we removed the name key from data already
|
||||
ret = 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)
|
||||
else:
|
||||
ret = Activity(**data)
|
||||
|
||||
if isinstance(ret.emoji, PartialEmoji):
|
||||
ret.emoji._state = state
|
||||
return ret
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
discord.app_commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Application commands support for the Discord API
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
from .commands import *
|
||||
from .errors import *
|
||||
from .models import *
|
||||
from .tree import *
|
||||
from .namespace import *
|
||||
from .transformers import *
|
||||
from .translator import *
|
||||
from .installs import *
|
||||
from . import checks as checks
|
||||
from .checks import Cooldown as Cooldown
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
537
venv/lib/python3.12/site-packages/discord/app_commands/checks.py
Normal file
537
venv/lib/python3.12/site-packages/discord/app_commands/checks.py
Normal file
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 (
|
||||
Any,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Hashable,
|
||||
Union,
|
||||
Callable,
|
||||
TypeVar,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
from .commands import check
|
||||
from .errors import (
|
||||
NoPrivateMessage,
|
||||
MissingRole,
|
||||
MissingAnyRole,
|
||||
MissingPermissions,
|
||||
BotMissingPermissions,
|
||||
CommandOnCooldown,
|
||||
)
|
||||
|
||||
from ..user import User
|
||||
from ..permissions import Permissions
|
||||
from ..utils import get as utils_get, MISSING, maybe_coroutine
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self, Unpack
|
||||
from ..interactions import Interaction
|
||||
from ..permissions import _PermissionsKwargs
|
||||
|
||||
CooldownFunction = Union[
|
||||
Callable[[Interaction[Any]], Coroutine[Any, Any, T]],
|
||||
Callable[[Interaction[Any]], T],
|
||||
]
|
||||
|
||||
__all__ = (
|
||||
'has_role',
|
||||
'has_any_role',
|
||||
'has_permissions',
|
||||
'bot_has_permissions',
|
||||
'cooldown',
|
||||
'dynamic_cooldown',
|
||||
)
|
||||
|
||||
|
||||
class Cooldown:
|
||||
"""Represents a cooldown for a command.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
rate: :class:`float`
|
||||
The total number of tokens available per :attr:`per` seconds.
|
||||
per: :class:`float`
|
||||
The length of the cooldown period in seconds.
|
||||
"""
|
||||
|
||||
__slots__ = ('rate', 'per', '_window', '_tokens', '_last')
|
||||
|
||||
def __init__(self, rate: float, per: float) -> None:
|
||||
self.rate: int = int(rate)
|
||||
self.per: float = float(per)
|
||||
self._window: float = 0.0
|
||||
self._tokens: int = self.rate
|
||||
self._last: float = 0.0
|
||||
|
||||
def get_tokens(self, current: Optional[float] = None) -> int:
|
||||
"""Returns the number of available tokens before rate limiting is applied.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
current: Optional[:class:`float`]
|
||||
The time in seconds since Unix epoch to calculate tokens at.
|
||||
If not supplied then :func:`time.time()` is used.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of tokens available before the cooldown is to be applied.
|
||||
"""
|
||||
if not current:
|
||||
current = time.time()
|
||||
|
||||
# the calculated tokens should be non-negative
|
||||
tokens = max(self._tokens, 0)
|
||||
|
||||
if current > self._window + self.per:
|
||||
tokens = self.rate
|
||||
return tokens
|
||||
|
||||
def get_retry_after(self, current: Optional[float] = None) -> float:
|
||||
"""Returns the time in seconds until the cooldown will be reset.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
current: Optional[:class:`float`]
|
||||
The current time in seconds since Unix epoch.
|
||||
If not supplied, then :func:`time.time()` is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`float`
|
||||
The number of seconds to wait before this cooldown will be reset.
|
||||
"""
|
||||
current = current or time.time()
|
||||
tokens = self.get_tokens(current)
|
||||
|
||||
if tokens == 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
return 0.0
|
||||
|
||||
def update_rate_limit(self, current: Optional[float] = None, *, tokens: int = 1) -> Optional[float]:
|
||||
"""Updates the cooldown rate limit.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
current: Optional[:class:`float`]
|
||||
The time in seconds since Unix epoch to update the rate limit at.
|
||||
If not supplied, then :func:`time.time()` is used.
|
||||
tokens: :class:`int`
|
||||
The amount of tokens to deduct from the rate limit.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`float`]
|
||||
The retry-after time in seconds if rate limited.
|
||||
"""
|
||||
current = current or time.time()
|
||||
self._last = current
|
||||
|
||||
self._tokens = self.get_tokens(current)
|
||||
|
||||
# first token used means that we start a new rate limit window
|
||||
if self._tokens == self.rate:
|
||||
self._window = current
|
||||
|
||||
# decrement tokens by specified number
|
||||
self._tokens -= tokens
|
||||
|
||||
# check if we are rate limited and return retry-after
|
||||
if self._tokens < 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the cooldown to its initial state."""
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Creates a copy of this cooldown.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Cooldown`
|
||||
A new instance of this cooldown.
|
||||
"""
|
||||
return self.__class__(self.rate, self.per)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Cooldown rate: {self.rate} per: {self.per} window: {self._window} tokens: {self._tokens}>'
|
||||
|
||||
|
||||
def has_role(item: Union[int, str], /) -> Callable[[T], T]:
|
||||
"""A :func:`~discord.app_commands.check` that is added that checks if the member invoking the
|
||||
command has the role specified via the name or ID specified.
|
||||
|
||||
If a string is specified, you must give the exact name of the role, including
|
||||
caps and spelling.
|
||||
|
||||
If an integer is specified, you must give the exact snowflake ID of the role.
|
||||
|
||||
This check raises one of two special exceptions, :exc:`~discord.app_commands.MissingRole`
|
||||
if the user is missing a role, or :exc:`~discord.app_commands.NoPrivateMessage` if
|
||||
it is used in a private message. Both inherit from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. note::
|
||||
|
||||
This is different from the permission system that Discord provides for application
|
||||
commands. This is done entirely locally in the program rather than being handled
|
||||
by Discord.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
item: Union[:class:`int`, :class:`str`]
|
||||
The name or ID of the role to check.
|
||||
"""
|
||||
|
||||
def predicate(interaction: Interaction) -> bool:
|
||||
if isinstance(interaction.user, User):
|
||||
raise NoPrivateMessage()
|
||||
|
||||
if isinstance(item, int):
|
||||
role = interaction.user.get_role(item)
|
||||
else:
|
||||
role = utils_get(interaction.user.roles, name=item)
|
||||
|
||||
if role is None:
|
||||
raise MissingRole(item)
|
||||
return True
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def has_any_role(*items: Union[int, str]) -> Callable[[T], T]:
|
||||
r"""A :func:`~discord.app_commands.check` that is added that checks if the member
|
||||
invoking the command has **any** of the roles specified. This means that if they have
|
||||
one out of the three roles specified, then this check will return ``True``.
|
||||
|
||||
Similar to :func:`has_role`\, the names or IDs passed in must be exact.
|
||||
|
||||
This check raises one of two special exceptions, :exc:`~discord.app_commands.MissingAnyRole`
|
||||
if the user is missing all roles, or :exc:`~discord.app_commands.NoPrivateMessage` if
|
||||
it is used in a private message. Both inherit from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. note::
|
||||
|
||||
This is different from the permission system that Discord provides for application
|
||||
commands. This is done entirely locally in the program rather than being handled
|
||||
by Discord.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
items: List[Union[:class:`str`, :class:`int`]]
|
||||
An argument list of names or IDs to check that the member has roles wise.
|
||||
|
||||
Example
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@tree.command()
|
||||
@app_commands.checks.has_any_role('Library Devs', 'Moderators', 492212595072434186)
|
||||
async def cool(interaction: discord.Interaction):
|
||||
await interaction.response.send_message('You are cool indeed')
|
||||
"""
|
||||
|
||||
def predicate(interaction: Interaction) -> bool:
|
||||
if isinstance(interaction.user, User):
|
||||
raise NoPrivateMessage()
|
||||
|
||||
if any(
|
||||
interaction.user.get_role(item) is not None
|
||||
if isinstance(item, int)
|
||||
else utils_get(interaction.user.roles, name=item) is not None
|
||||
for item in items
|
||||
):
|
||||
return True
|
||||
raise MissingAnyRole(list(items))
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
|
||||
r"""A :func:`~discord.app_commands.check` that is added that checks if the member
|
||||
has all of the permissions necessary.
|
||||
|
||||
Note that this check operates on the permissions given by
|
||||
:attr:`discord.Interaction.permissions`.
|
||||
|
||||
The permissions passed in must be exactly like the properties shown under
|
||||
:class:`discord.Permissions`.
|
||||
|
||||
This check raises a special exception, :exc:`~discord.app_commands.MissingPermissions`
|
||||
that is inherited from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. note::
|
||||
|
||||
This is different from the permission system that Discord provides for application
|
||||
commands. This is done entirely locally in the program rather than being handled
|
||||
by Discord.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*\*perms: :class:`bool`
|
||||
Keyword arguments denoting the permissions to check for.
|
||||
|
||||
Example
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@tree.command()
|
||||
@app_commands.checks.has_permissions(manage_messages=True)
|
||||
async def test(interaction: discord.Interaction):
|
||||
await interaction.response.send_message('You can manage messages.')
|
||||
|
||||
"""
|
||||
|
||||
invalid = perms.keys() - Permissions.VALID_FLAGS.keys()
|
||||
if invalid:
|
||||
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
|
||||
|
||||
def predicate(interaction: Interaction) -> bool:
|
||||
permissions = interaction.permissions
|
||||
|
||||
missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value]
|
||||
|
||||
if not missing:
|
||||
return True
|
||||
|
||||
raise MissingPermissions(missing)
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def bot_has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
|
||||
"""Similar to :func:`has_permissions` except checks if the bot itself has
|
||||
the permissions listed. This relies on :attr:`discord.Interaction.app_permissions`.
|
||||
|
||||
This check raises a special exception, :exc:`~discord.app_commands.BotMissingPermissions`
|
||||
that is inherited from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
invalid = set(perms) - set(Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
|
||||
|
||||
def predicate(interaction: Interaction) -> bool:
|
||||
permissions = interaction.app_permissions
|
||||
missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value]
|
||||
|
||||
if not missing:
|
||||
return True
|
||||
|
||||
raise BotMissingPermissions(missing)
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def _create_cooldown_decorator(
|
||||
key: CooldownFunction[Hashable], factory: CooldownFunction[Optional[Cooldown]]
|
||||
) -> Callable[[T], T]:
|
||||
mapping: Dict[Any, Cooldown] = {}
|
||||
|
||||
async def get_bucket(
|
||||
interaction: Interaction,
|
||||
*,
|
||||
mapping: Dict[Any, Cooldown] = mapping,
|
||||
key: CooldownFunction[Hashable] = key,
|
||||
factory: CooldownFunction[Optional[Cooldown]] = factory,
|
||||
) -> Optional[Cooldown]:
|
||||
current = interaction.created_at.timestamp()
|
||||
dead_keys = [k for k, v in mapping.items() if current > v._last + v.per]
|
||||
for k in dead_keys:
|
||||
del mapping[k]
|
||||
|
||||
k = await maybe_coroutine(key, interaction)
|
||||
if k not in mapping:
|
||||
bucket: Optional[Cooldown] = await maybe_coroutine(factory, interaction)
|
||||
if bucket is not None:
|
||||
mapping[k] = bucket
|
||||
else:
|
||||
bucket = mapping[k]
|
||||
|
||||
return bucket
|
||||
|
||||
async def predicate(interaction: Interaction) -> bool:
|
||||
bucket = await get_bucket(interaction)
|
||||
if bucket is None:
|
||||
return True
|
||||
|
||||
retry_after = bucket.update_rate_limit(interaction.created_at.timestamp())
|
||||
if retry_after is None:
|
||||
return True
|
||||
|
||||
raise CommandOnCooldown(bucket, retry_after)
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def cooldown(
|
||||
rate: float,
|
||||
per: float,
|
||||
*,
|
||||
key: Optional[CooldownFunction[Hashable]] = MISSING,
|
||||
) -> Callable[[T], T]:
|
||||
"""A decorator that adds a cooldown to a command.
|
||||
|
||||
A cooldown allows a command to only be used a specific amount
|
||||
of times in a specific time frame. These cooldowns are based off
|
||||
of the ``key`` function provided. If a ``key`` is not provided
|
||||
then it defaults to a user-level cooldown. The ``key`` function
|
||||
must take a single parameter, the :class:`discord.Interaction` and
|
||||
return a value that is used as a key to the internal cooldown mapping.
|
||||
|
||||
The ``key`` function can optionally be a coroutine.
|
||||
|
||||
If a cooldown is triggered, then :exc:`~discord.app_commands.CommandOnCooldown` is
|
||||
raised to the error handlers.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Setting a one per 5 seconds per member cooldown on a command:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@tree.command()
|
||||
@app_commands.checks.cooldown(1, 5.0, key=lambda i: (i.guild_id, i.user.id))
|
||||
async def test(interaction: discord.Interaction):
|
||||
await interaction.response.send_message('Hello')
|
||||
|
||||
@test.error
|
||||
async def on_test_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
|
||||
if isinstance(error, app_commands.CommandOnCooldown):
|
||||
await interaction.response.send_message(str(error), ephemeral=True)
|
||||
|
||||
Parameters
|
||||
------------
|
||||
rate: :class:`int`
|
||||
The number of times a command can be used before triggering a cooldown.
|
||||
per: :class:`float`
|
||||
The amount of seconds to wait for a cooldown when it's been triggered.
|
||||
key: Optional[Callable[[:class:`discord.Interaction`], :class:`collections.abc.Hashable`]]
|
||||
A function that returns a key to the mapping denoting the type of cooldown.
|
||||
Can optionally be a coroutine. If not given then defaults to a user-level
|
||||
cooldown. If ``None`` is passed then it is interpreted as a "global" cooldown.
|
||||
"""
|
||||
|
||||
if key is MISSING:
|
||||
key_func = lambda interaction: interaction.user.id
|
||||
elif key is None:
|
||||
key_func = lambda i: None
|
||||
else:
|
||||
key_func = key
|
||||
|
||||
factory = lambda interaction: Cooldown(rate, per)
|
||||
|
||||
return _create_cooldown_decorator(key_func, factory)
|
||||
|
||||
|
||||
def dynamic_cooldown(
|
||||
factory: CooldownFunction[Optional[Cooldown]],
|
||||
*,
|
||||
key: Optional[CooldownFunction[Hashable]] = MISSING,
|
||||
) -> Callable[[T], T]:
|
||||
"""A decorator that adds a dynamic cooldown to a command.
|
||||
|
||||
A cooldown allows a command to only be used a specific amount
|
||||
of times in a specific time frame. These cooldowns are based off
|
||||
of the ``key`` function provided. If a ``key`` is not provided
|
||||
then it defaults to a user-level cooldown. The ``key`` function
|
||||
must take a single parameter, the :class:`discord.Interaction` and
|
||||
return a value that is used as a key to the internal cooldown mapping.
|
||||
|
||||
If a ``factory`` function is given, it must be a function that
|
||||
accepts a single parameter of type :class:`discord.Interaction` and must
|
||||
return a :class:`~discord.app_commands.Cooldown` or ``None``.
|
||||
If ``None`` is returned then that cooldown is effectively bypassed.
|
||||
|
||||
Both ``key`` and ``factory`` can optionally be coroutines.
|
||||
|
||||
If a cooldown is triggered, then :exc:`~discord.app_commands.CommandOnCooldown` is
|
||||
raised to the error handlers.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Setting a cooldown for everyone but the owner.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
def cooldown_for_everyone_but_me(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
|
||||
if interaction.user.id == 80088516616269824:
|
||||
return None
|
||||
return app_commands.Cooldown(1, 10.0)
|
||||
|
||||
@tree.command()
|
||||
@app_commands.checks.dynamic_cooldown(cooldown_for_everyone_but_me)
|
||||
async def test(interaction: discord.Interaction):
|
||||
await interaction.response.send_message('Hello')
|
||||
|
||||
@test.error
|
||||
async def on_test_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
|
||||
if isinstance(error, app_commands.CommandOnCooldown):
|
||||
await interaction.response.send_message(str(error), ephemeral=True)
|
||||
|
||||
Parameters
|
||||
------------
|
||||
factory: Optional[Callable[[:class:`discord.Interaction`], Optional[:class:`~discord.app_commands.Cooldown`]]]
|
||||
A function that takes an interaction and returns a cooldown that will apply to that interaction
|
||||
or ``None`` if the interaction should not have a cooldown.
|
||||
key: Optional[Callable[[:class:`discord.Interaction`], :class:`collections.abc.Hashable`]]
|
||||
A function that returns a key to the mapping denoting the type of cooldown.
|
||||
Can optionally be a coroutine. If not given then defaults to a user-level
|
||||
cooldown. If ``None`` is passed then it is interpreted as a "global" cooldown.
|
||||
"""
|
||||
|
||||
if key is MISSING:
|
||||
key_func = lambda interaction: interaction.user.id
|
||||
elif key is None:
|
||||
key_func = lambda i: None
|
||||
else:
|
||||
key_func = key
|
||||
|
||||
return _create_cooldown_decorator(key_func, factory)
|
||||
2884
venv/lib/python3.12/site-packages/discord/app_commands/commands.py
Normal file
2884
venv/lib/python3.12/site-packages/discord/app_commands/commands.py
Normal file
File diff suppressed because it is too large
Load Diff
519
venv/lib/python3.12/site-packages/discord/app_commands/errors.py
Normal file
519
venv/lib/python3.12/site-packages/discord/app_commands/errors.py
Normal file
@@ -0,0 +1,519 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 Any, TYPE_CHECKING, List, Optional, Sequence, Union
|
||||
|
||||
from ..enums import AppCommandOptionType, AppCommandType, Locale
|
||||
from ..errors import DiscordException, HTTPException, _flatten_error_dict, MissingApplicationID as MissingApplicationID
|
||||
from ..utils import _human_join
|
||||
|
||||
__all__ = (
|
||||
'AppCommandError',
|
||||
'CommandInvokeError',
|
||||
'TransformerError',
|
||||
'TranslationError',
|
||||
'CheckFailure',
|
||||
'CommandAlreadyRegistered',
|
||||
'CommandSignatureMismatch',
|
||||
'CommandNotFound',
|
||||
'CommandLimitReached',
|
||||
'NoPrivateMessage',
|
||||
'MissingRole',
|
||||
'MissingAnyRole',
|
||||
'MissingPermissions',
|
||||
'BotMissingPermissions',
|
||||
'CommandOnCooldown',
|
||||
'MissingApplicationID',
|
||||
'CommandSyncFailure',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command, Group, ContextMenu, Parameter
|
||||
from .transformers import Transformer
|
||||
from .translator import TranslationContextTypes, locale_str
|
||||
from ..types.snowflake import Snowflake, SnowflakeList
|
||||
from .checks import Cooldown
|
||||
|
||||
CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu]
|
||||
|
||||
|
||||
class AppCommandError(DiscordException):
|
||||
"""The base exception type for all application command related errors.
|
||||
|
||||
This inherits from :exc:`discord.DiscordException`.
|
||||
|
||||
This exception and exceptions inherited from it are handled
|
||||
in a special way as they are caught and passed into various error handlers
|
||||
in this order:
|
||||
|
||||
- :meth:`Command.error <discord.app_commands.Command.error>`
|
||||
- :meth:`Group.on_error <discord.app_commands.Group.on_error>`
|
||||
- :meth:`CommandTree.on_error <discord.app_commands.CommandTree.on_error>`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandInvokeError(AppCommandError):
|
||||
"""An exception raised when the command being invoked raised an exception.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
command: Union[:class:`Command`, :class:`ContextMenu`]
|
||||
The command that failed.
|
||||
"""
|
||||
|
||||
def __init__(self, command: Union[Command[Any, ..., Any], ContextMenu], e: Exception) -> None:
|
||||
self.original: Exception = e
|
||||
self.command: Union[Command[Any, ..., Any], ContextMenu] = command
|
||||
super().__init__(f'Command {command.name!r} raised an exception: {e.__class__.__name__}: {e}')
|
||||
|
||||
|
||||
class TransformerError(AppCommandError):
|
||||
"""An exception raised when a :class:`Transformer` or type annotation fails to
|
||||
convert to its target type.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
If an exception occurs while converting that does not subclass
|
||||
:exc:`AppCommandError` then the exception is wrapped into this exception.
|
||||
The original exception can be retrieved using the ``__cause__`` attribute.
|
||||
Otherwise if the exception derives from :exc:`AppCommandError` then it will
|
||||
be propagated as-is.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: Any
|
||||
The value that failed to convert.
|
||||
type: :class:`~discord.AppCommandOptionType`
|
||||
The type of argument that failed to convert.
|
||||
transformer: :class:`Transformer`
|
||||
The transformer that failed the conversion.
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any, opt_type: AppCommandOptionType, transformer: Transformer):
|
||||
self.value: Any = value
|
||||
self.type: AppCommandOptionType = opt_type
|
||||
self.transformer: Transformer = transformer
|
||||
|
||||
super().__init__(f'Failed to convert {value} to {transformer._error_display_name!s}')
|
||||
|
||||
|
||||
class TranslationError(AppCommandError):
|
||||
"""An exception raised when the library fails to translate a string.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
If an exception occurs while calling :meth:`Translator.translate` that does
|
||||
not subclass this then the exception is wrapped into this exception.
|
||||
The original exception can be retrieved using the ``__cause__`` attribute.
|
||||
Otherwise it will be propagated as-is.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
string: Optional[Union[:class:`str`, :class:`locale_str`]]
|
||||
The string that caused the error, if any.
|
||||
locale: Optional[:class:`~discord.Locale`]
|
||||
The locale that caused the error, if any.
|
||||
context: :class:`~discord.app_commands.TranslationContext`
|
||||
The context of the translation that triggered the error.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*msg: str,
|
||||
string: Optional[Union[str, locale_str]] = None,
|
||||
locale: Optional[Locale] = None,
|
||||
context: TranslationContextTypes,
|
||||
) -> None:
|
||||
self.string: Optional[Union[str, locale_str]] = string
|
||||
self.locale: Optional[Locale] = locale
|
||||
self.context: TranslationContextTypes = context
|
||||
|
||||
if msg:
|
||||
super().__init__(*msg)
|
||||
else:
|
||||
ctx = context.location.name.replace('_', ' ')
|
||||
fmt = f'Failed to translate {self.string!r} in a {ctx}'
|
||||
if self.locale is not None:
|
||||
fmt = f'{fmt} in the {self.locale.value} locale'
|
||||
|
||||
super().__init__(fmt)
|
||||
|
||||
|
||||
class CheckFailure(AppCommandError):
|
||||
"""An exception raised when check predicates in a command have failed.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoPrivateMessage(CheckFailure):
|
||||
"""An exception raised when a command does not work in a direct message.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def __init__(self, message: Optional[str] = None) -> None:
|
||||
super().__init__(message or 'This command cannot be used in direct messages.')
|
||||
|
||||
|
||||
class MissingRole(CheckFailure):
|
||||
"""An exception raised when the command invoker lacks a role to run a command.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_role: Union[:class:`str`, :class:`int`]
|
||||
The required role that is missing.
|
||||
This is the parameter passed to :func:`~discord.app_commands.checks.has_role`.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_role: Snowflake) -> None:
|
||||
self.missing_role: Snowflake = missing_role
|
||||
message = f'Role {missing_role!r} is required to run this command.'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class MissingAnyRole(CheckFailure):
|
||||
"""An exception raised when the command invoker lacks any of the roles
|
||||
specified to run a command.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_roles: List[Union[:class:`str`, :class:`int`]]
|
||||
The roles that the invoker is missing.
|
||||
These are the parameters passed to :func:`~discord.app_commands.checks.has_any_role`.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_roles: SnowflakeList) -> None:
|
||||
self.missing_roles: SnowflakeList = missing_roles
|
||||
|
||||
fmt = _human_join([f"'{role}'" for role in missing_roles])
|
||||
message = f'You are missing at least one of the required roles: {fmt}'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""An exception raised when the command invoker lacks permissions to run a
|
||||
command.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_permissions: List[:class:`str`]
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_permissions: List[str], *args: Any) -> None:
|
||||
self.missing_permissions: List[str] = missing_permissions
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
|
||||
fmt = _human_join(missing, final='and')
|
||||
message = f'You are missing {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
"""An exception raised when the bot's member lacks permissions to run a
|
||||
command.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_permissions: List[:class:`str`]
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_permissions: List[str], *args: Any) -> None:
|
||||
self.missing_permissions: List[str] = missing_permissions
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
|
||||
fmt = _human_join(missing, final='and')
|
||||
message = f'Bot requires {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
|
||||
class CommandOnCooldown(CheckFailure):
|
||||
"""An exception raised when the command being invoked is on cooldown.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.CheckFailure`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
cooldown: :class:`~discord.app_commands.Cooldown`
|
||||
The cooldown that was triggered.
|
||||
retry_after: :class:`float`
|
||||
The amount of seconds to wait before you can retry again.
|
||||
"""
|
||||
|
||||
def __init__(self, cooldown: Cooldown, retry_after: float) -> None:
|
||||
self.cooldown: Cooldown = cooldown
|
||||
self.retry_after: float = retry_after
|
||||
super().__init__(f'You are on cooldown. Try again in {retry_after:.2f}s')
|
||||
|
||||
|
||||
class CommandAlreadyRegistered(AppCommandError):
|
||||
"""An exception raised when a command is already registered.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the command already registered.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID this command was already registered at.
|
||||
If ``None`` then it was a global command.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, guild_id: Optional[int]):
|
||||
self.name: str = name
|
||||
self.guild_id: Optional[int] = guild_id
|
||||
super().__init__(f'Command {name!r} already registered.')
|
||||
|
||||
|
||||
class CommandNotFound(AppCommandError):
|
||||
"""An exception raised when an application command could not be found.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the application command not found.
|
||||
parents: List[:class:`str`]
|
||||
A list of parent command names that were previously found
|
||||
prior to the application command not being found.
|
||||
type: :class:`~discord.AppCommandType`
|
||||
The type of command that was not found.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, parents: List[str], type: AppCommandType = AppCommandType.chat_input):
|
||||
self.name: str = name
|
||||
self.parents: List[str] = parents
|
||||
self.type: AppCommandType = type
|
||||
super().__init__(f'Application command {name!r} not found')
|
||||
|
||||
|
||||
class CommandLimitReached(AppCommandError):
|
||||
"""An exception raised when the maximum number of application commands was reached
|
||||
either globally or in a guild.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
type: :class:`~discord.AppCommandType`
|
||||
The type of command that reached the limit.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID that reached the limit or ``None`` if it was global.
|
||||
limit: :class:`int`
|
||||
The limit that was hit.
|
||||
"""
|
||||
|
||||
def __init__(self, guild_id: Optional[int], limit: int, type: AppCommandType = AppCommandType.chat_input):
|
||||
self.guild_id: Optional[int] = guild_id
|
||||
self.limit: int = limit
|
||||
self.type: AppCommandType = type
|
||||
|
||||
lookup = {
|
||||
AppCommandType.chat_input: 'slash commands',
|
||||
AppCommandType.message: 'message context menu commands',
|
||||
AppCommandType.user: 'user context menu commands',
|
||||
}
|
||||
desc = lookup.get(type, 'application commands')
|
||||
ns = 'globally' if self.guild_id is None else f'for guild ID {self.guild_id}'
|
||||
super().__init__(f'maximum number of {desc} exceeded {limit} {ns}')
|
||||
|
||||
|
||||
class CommandSignatureMismatch(AppCommandError):
|
||||
"""An exception raised when an application command from Discord has a different signature
|
||||
from the one provided in the code. This happens because your command definition differs
|
||||
from the command definition you provided Discord. Either your code is out of date or the
|
||||
data from Discord is out of sync.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
command: Union[:class:`~.app_commands.Command`, :class:`~.app_commands.ContextMenu`, :class:`~.app_commands.Group`]
|
||||
The command that had the signature mismatch.
|
||||
"""
|
||||
|
||||
def __init__(self, command: Union[Command[Any, ..., Any], ContextMenu, Group]):
|
||||
self.command: Union[Command[Any, ..., Any], ContextMenu, Group] = command
|
||||
msg = (
|
||||
f'The signature for command {command.name!r} is different from the one provided by Discord. '
|
||||
'This can happen because either your code is out of date or you have not synced the '
|
||||
'commands with Discord, causing the mismatch in data. It is recommended to sync the '
|
||||
'command tree to fix this issue.'
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def _get_command_error(
|
||||
index: str,
|
||||
inner: Any,
|
||||
objects: Sequence[Union[Parameter, CommandTypes]],
|
||||
messages: List[str],
|
||||
indent: int = 0,
|
||||
) -> None:
|
||||
# Import these here to avoid circular imports
|
||||
from .commands import Command, Group, ContextMenu
|
||||
|
||||
indentation = ' ' * indent
|
||||
|
||||
# Top level errors are:
|
||||
# <number>: { <key>: <error> }
|
||||
# The dicts could be nested, e.g.
|
||||
# <number>: { <key>: { <second>: <error> } }
|
||||
# Luckily, this is already handled by the flatten_error_dict utility
|
||||
if not index.isdigit():
|
||||
errors = _flatten_error_dict(inner, index)
|
||||
messages.extend(f'In {k}: {v}' for k, v in errors.items())
|
||||
return
|
||||
|
||||
idx = int(index)
|
||||
try:
|
||||
obj = objects[idx]
|
||||
except IndexError:
|
||||
dedent_one_level = ' ' * (indent - 2)
|
||||
errors = _flatten_error_dict(inner, index)
|
||||
messages.extend(f'{dedent_one_level}In {k}: {v}' for k, v in errors.items())
|
||||
return
|
||||
|
||||
children: Sequence[Union[Parameter, CommandTypes]] = []
|
||||
if isinstance(obj, Command):
|
||||
messages.append(f'{indentation}In command {obj.qualified_name!r} defined in function {obj.callback.__qualname__!r}')
|
||||
children = obj.parameters
|
||||
elif isinstance(obj, Group):
|
||||
messages.append(f'{indentation}In group {obj.qualified_name!r} defined in module {obj.module!r}')
|
||||
children = obj.commands
|
||||
elif isinstance(obj, ContextMenu):
|
||||
messages.append(
|
||||
f'{indentation}In context menu {obj.qualified_name!r} defined in function {obj.callback.__qualname__!r}'
|
||||
)
|
||||
else:
|
||||
messages.append(f'{indentation}In parameter {obj.name!r}')
|
||||
|
||||
for key, remaining in inner.items():
|
||||
# Special case the 'options' key since they have well defined meanings
|
||||
if key == 'options':
|
||||
for index, d in remaining.items():
|
||||
_get_command_error(index, d, children, messages, indent=indent + 2)
|
||||
elif key == '_errors':
|
||||
errors = [x.get('message', '') for x in remaining]
|
||||
|
||||
messages.extend(f'{indentation} {message}' for message in errors)
|
||||
else:
|
||||
if isinstance(remaining, dict):
|
||||
try:
|
||||
inner_errors = remaining['_errors']
|
||||
except KeyError:
|
||||
errors = _flatten_error_dict(remaining, key=key)
|
||||
else:
|
||||
errors = {key: ' '.join(x.get('message', '') for x in inner_errors)}
|
||||
|
||||
if isinstance(errors, dict):
|
||||
messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items())
|
||||
|
||||
|
||||
class CommandSyncFailure(AppCommandError, HTTPException):
|
||||
"""An exception raised when :meth:`CommandTree.sync` failed.
|
||||
|
||||
This provides syncing failures in a slightly more readable format.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`
|
||||
and :exc:`~discord.HTTPException`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def __init__(self, child: HTTPException, commands: List[CommandTypes]) -> None:
|
||||
# Consume the child exception and make it seem as if we are that exception
|
||||
self.__dict__.update(child.__dict__)
|
||||
|
||||
messages = [f'Failed to upload commands to Discord (HTTP status {self.status}, error code {self.code})']
|
||||
|
||||
if self._errors:
|
||||
# Handle case where the errors dict has no actual chain such as APPLICATION_COMMAND_TOO_LARGE
|
||||
if len(self._errors) == 1 and '_errors' in self._errors:
|
||||
errors = self._errors['_errors']
|
||||
if len(errors) == 1:
|
||||
extra = errors[0].get('message')
|
||||
if extra:
|
||||
messages[0] += f': {extra}'
|
||||
else:
|
||||
messages.extend(f'Error {e.get("code", "")}: {e.get("message", "")}' for e in errors)
|
||||
else:
|
||||
for index, inner in self._errors.items():
|
||||
_get_command_error(index, inner, commands, messages)
|
||||
|
||||
# Equivalent to super().__init__(...) but skips other constructors
|
||||
self.args = ('\n'.join(messages),)
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, ClassVar, List, Optional, Sequence
|
||||
|
||||
__all__ = (
|
||||
'AppInstallationType',
|
||||
'AppCommandContext',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from ..types.interactions import InteractionContextType, InteractionInstallationType
|
||||
|
||||
|
||||
class AppInstallationType:
|
||||
r"""Represents the installation location of an application command.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
guild: Optional[:class:`bool`]
|
||||
Whether the integration is a guild install.
|
||||
user: Optional[:class:`bool`]
|
||||
Whether the integration is a user install.
|
||||
"""
|
||||
|
||||
__slots__ = ('_guild', '_user')
|
||||
|
||||
GUILD: ClassVar[int] = 0
|
||||
USER: ClassVar[int] = 1
|
||||
|
||||
def __init__(self, *, guild: Optional[bool] = None, user: Optional[bool] = None):
|
||||
self._guild: Optional[bool] = guild
|
||||
self._user: Optional[bool] = user
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AppInstallationType guild={self.guild!r} user={self.user!r}>'
|
||||
|
||||
@property
|
||||
def guild(self) -> bool:
|
||||
""":class:`bool`: Whether the integration is a guild install."""
|
||||
return bool(self._guild)
|
||||
|
||||
@guild.setter
|
||||
def guild(self, value: bool) -> None:
|
||||
self._guild = bool(value)
|
||||
|
||||
@property
|
||||
def user(self) -> bool:
|
||||
""":class:`bool`: Whether the integration is a user install."""
|
||||
return bool(self._user)
|
||||
|
||||
@user.setter
|
||||
def user(self, value: bool) -> None:
|
||||
self._user = bool(value)
|
||||
|
||||
def merge(self, other: AppInstallationType) -> AppInstallationType:
|
||||
# Merging is similar to AllowedMentions where `self` is the base
|
||||
# and the `other` is the override preference
|
||||
guild = self._guild if other._guild is None else other._guild
|
||||
user = self._user if other._user is None else other._user
|
||||
return AppInstallationType(guild=guild, user=user)
|
||||
|
||||
def _is_unset(self) -> bool:
|
||||
return all(x is None for x in (self._guild, self._user))
|
||||
|
||||
def _merge_to_array(self, other: Optional[AppInstallationType]) -> Optional[List[InteractionInstallationType]]:
|
||||
result = self.merge(other) if other is not None else self
|
||||
if result._is_unset():
|
||||
return None
|
||||
return result.to_array()
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls, value: Sequence[InteractionInstallationType]) -> Self:
|
||||
self = cls()
|
||||
for x in value:
|
||||
if x == cls.GUILD:
|
||||
self._guild = True
|
||||
elif x == cls.USER:
|
||||
self._user = True
|
||||
return self
|
||||
|
||||
def to_array(self) -> List[InteractionInstallationType]:
|
||||
values = []
|
||||
if self._guild:
|
||||
values.append(self.GUILD)
|
||||
if self._user:
|
||||
values.append(self.USER)
|
||||
return values
|
||||
|
||||
|
||||
class AppCommandContext:
|
||||
r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
guild: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a guild.
|
||||
dm_channel: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a DM channel.
|
||||
private_channel: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a DM or a GDM channel.
|
||||
"""
|
||||
|
||||
GUILD: ClassVar[int] = 0
|
||||
DM_CHANNEL: ClassVar[int] = 1
|
||||
PRIVATE_CHANNEL: ClassVar[int] = 2
|
||||
|
||||
__slots__ = ('_guild', '_dm_channel', '_private_channel')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
guild: Optional[bool] = None,
|
||||
dm_channel: Optional[bool] = None,
|
||||
private_channel: Optional[bool] = None,
|
||||
):
|
||||
self._guild: Optional[bool] = guild
|
||||
self._dm_channel: Optional[bool] = dm_channel
|
||||
self._private_channel: Optional[bool] = private_channel
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AppCommandContext guild={self.guild!r} dm_channel={self.dm_channel!r} private_channel={self.private_channel!r}>'
|
||||
|
||||
@property
|
||||
def guild(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a guild."""
|
||||
return bool(self._guild)
|
||||
|
||||
@guild.setter
|
||||
def guild(self, value: bool) -> None:
|
||||
self._guild = bool(value)
|
||||
|
||||
@property
|
||||
def dm_channel(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a DM channel."""
|
||||
return bool(self._dm_channel)
|
||||
|
||||
@dm_channel.setter
|
||||
def dm_channel(self, value: bool) -> None:
|
||||
self._dm_channel = bool(value)
|
||||
|
||||
@property
|
||||
def private_channel(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a DM or a GDM channel."""
|
||||
return bool(self._private_channel)
|
||||
|
||||
@private_channel.setter
|
||||
def private_channel(self, value: bool) -> None:
|
||||
self._private_channel = bool(value)
|
||||
|
||||
def merge(self, other: AppCommandContext) -> AppCommandContext:
|
||||
guild = self._guild if other._guild is None else other._guild
|
||||
dm_channel = self._dm_channel if other._dm_channel is None else other._dm_channel
|
||||
private_channel = self._private_channel if other._private_channel is None else other._private_channel
|
||||
return AppCommandContext(guild=guild, dm_channel=dm_channel, private_channel=private_channel)
|
||||
|
||||
def _is_unset(self) -> bool:
|
||||
return all(x is None for x in (self._guild, self._dm_channel, self._private_channel))
|
||||
|
||||
def _merge_to_array(self, other: Optional[AppCommandContext]) -> Optional[List[InteractionContextType]]:
|
||||
result = self.merge(other) if other is not None else self
|
||||
if result._is_unset():
|
||||
return None
|
||||
return result.to_array()
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls, value: Sequence[InteractionContextType]) -> Self:
|
||||
self = cls()
|
||||
for x in value:
|
||||
if x == cls.GUILD:
|
||||
self._guild = True
|
||||
elif x == cls.DM_CHANNEL:
|
||||
self._dm_channel = True
|
||||
elif x == cls.PRIVATE_CHANNEL:
|
||||
self._private_channel = True
|
||||
return self
|
||||
|
||||
def to_array(self) -> List[InteractionContextType]:
|
||||
values = []
|
||||
if self._guild:
|
||||
values.append(self.GUILD)
|
||||
if self._dm_channel:
|
||||
values.append(self.DM_CHANNEL)
|
||||
if self._private_channel:
|
||||
values.append(self.PRIVATE_CHANNEL)
|
||||
return values
|
||||
1293
venv/lib/python3.12/site-packages/discord/app_commands/models.py
Normal file
1293
venv/lib/python3.12/site-packages/discord/app_commands/models.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, Dict, Iterable, Iterator, List, NamedTuple, Tuple
|
||||
from ..member import Member
|
||||
from ..object import Object
|
||||
from ..role import Role
|
||||
from ..message import Message, Attachment
|
||||
from ..channel import PartialMessageable
|
||||
from ..enums import AppCommandOptionType
|
||||
from .models import AppCommandChannel, AppCommandThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption
|
||||
|
||||
__all__ = ('Namespace',)
|
||||
|
||||
|
||||
class ResolveKey(NamedTuple):
|
||||
id: str
|
||||
# CommandOptionType does not use 0 or negative numbers so those can be safe for library
|
||||
# internal use, if necessary. Likewise, only 6, 7, 8, and 11 are actually in use.
|
||||
type: int
|
||||
|
||||
@classmethod
|
||||
def any_with(cls, id: str) -> ResolveKey:
|
||||
return ResolveKey(id=id, type=-1)
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
if not isinstance(o, ResolveKey):
|
||||
return NotImplemented
|
||||
if self.type == -1 or o.type == -1:
|
||||
return self.id == o.id
|
||||
return (self.id, self.type) == (o.id, o.type)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Most of the time an ID lookup is all that is necessary
|
||||
# In case of collision then we look up both the ID and the type.
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class Namespace:
|
||||
"""An object that holds the parameters being passed to a command in a mostly raw state.
|
||||
|
||||
This class is deliberately simple and just holds the option name and resolved value as a simple
|
||||
key-pair mapping. These attributes can be accessed using dot notation. For example, an option
|
||||
with the name of ``example`` can be accessed using ``ns.example``. If an attribute is not found,
|
||||
then ``None`` is returned rather than an attribute error.
|
||||
|
||||
.. warning::
|
||||
|
||||
The key names come from the raw Discord data, which means that if a parameter was renamed then the
|
||||
renamed key is used instead of the function parameter name.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two namespaces are equal by checking if all attributes are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two namespaces are not equal.
|
||||
.. describe:: x[key]
|
||||
|
||||
Returns an attribute if it is found, otherwise raises
|
||||
a :exc:`KeyError`.
|
||||
.. describe:: key in x
|
||||
|
||||
Checks if the attribute is in the namespace.
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||
to be, for example, constructed as a dict or a list of pairs.
|
||||
|
||||
This namespace object converts resolved objects into their appropriate form depending on their
|
||||
type. Consult the table below for conversion information.
|
||||
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| Option Type | Resolved Type |
|
||||
+===========================================+===============================================================================+
|
||||
| :attr:`.AppCommandOptionType.string` | :class:`str` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.integer` | :class:`int` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.boolean` | :class:`bool` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.number` | :class:`float` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.user` | :class:`~discord.User` or :class:`~discord.Member` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.channel` | :class:`.AppCommandChannel` or :class:`.AppCommandThread` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.role` | :class:`~discord.Role` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.mentionable` | :class:`~discord.User` or :class:`~discord.Member`, or :class:`~discord.Role` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :attr:`.AppCommandOptionType.attachment` | :class:`~discord.Attachment` |
|
||||
+-------------------------------------------+-------------------------------------------------------------------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
In autocomplete interactions, the namespace might not be validated or filled in. Discord does not
|
||||
send the resolved data as well, so this means that certain fields end up just as IDs rather than
|
||||
the resolved data. In these cases, a :class:`discord.Object` is returned instead.
|
||||
|
||||
This is a Discord limitation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
interaction: Interaction,
|
||||
resolved: ResolvedData,
|
||||
options: List[ApplicationCommandInteractionDataOption],
|
||||
):
|
||||
completed = self._get_resolved_items(interaction, resolved)
|
||||
for option in options:
|
||||
opt_type = option['type']
|
||||
name = option['name']
|
||||
focused = option.get('focused', False)
|
||||
if opt_type in (3, 4, 5): # string, integer, boolean
|
||||
value = option['value'] # type: ignore # Key is there
|
||||
self.__dict__[name] = value
|
||||
elif opt_type == 10: # number
|
||||
value = option['value'] # type: ignore # Key is there
|
||||
# This condition is written this way because 0 can be a valid float
|
||||
if value is None or value == '':
|
||||
self.__dict__[name] = float('nan')
|
||||
else:
|
||||
if not focused:
|
||||
self.__dict__[name] = float(value)
|
||||
else:
|
||||
# Autocomplete focused values tend to be garbage in
|
||||
self.__dict__[name] = value
|
||||
elif opt_type in (6, 7, 8, 9, 11):
|
||||
# Remaining ones should be snowflake based ones with resolved data
|
||||
snowflake: str = option['value'] # type: ignore # Key is there
|
||||
if opt_type == 9: # Mentionable
|
||||
# Mentionable is User | Role, these do not cause any conflict
|
||||
key = ResolveKey.any_with(snowflake)
|
||||
else:
|
||||
# The remaining keys can conflict, for example, a role and a channel
|
||||
# could end up with the same ID in very old guilds since they used to default
|
||||
# to sharing the guild ID. Old general channels no longer exist, but some old
|
||||
# servers will still have them so this needs to be handled.
|
||||
key = ResolveKey(id=snowflake, type=opt_type)
|
||||
|
||||
value = completed.get(key) or Object(id=int(snowflake))
|
||||
self.__dict__[name] = value
|
||||
|
||||
@classmethod
|
||||
def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) -> Dict[ResolveKey, Any]:
|
||||
completed: Dict[ResolveKey, Any] = {}
|
||||
state = interaction._state
|
||||
members = resolved.get('members', {})
|
||||
guild_id = interaction.guild_id
|
||||
guild = interaction.guild
|
||||
type = AppCommandOptionType.user.value
|
||||
for user_id, user_data in resolved.get('users', {}).items():
|
||||
try:
|
||||
member_data = members[user_id]
|
||||
except KeyError:
|
||||
completed[ResolveKey(id=user_id, type=type)] = state.create_user(user_data)
|
||||
else:
|
||||
member_data['user'] = user_data
|
||||
# Guild ID can't be None in this case.
|
||||
# There's a type mismatch here that I don't actually care about
|
||||
member = Member(state=state, guild=guild, data=member_data) # type: ignore
|
||||
completed[ResolveKey(id=user_id, type=type)] = member
|
||||
|
||||
type = AppCommandOptionType.role.value
|
||||
completed.update(
|
||||
{
|
||||
# The guild ID can't be None in this case.
|
||||
ResolveKey(id=role_id, type=type): Role(guild=guild, state=state, data=role_data) # type: ignore
|
||||
for role_id, role_data in resolved.get('roles', {}).items()
|
||||
}
|
||||
)
|
||||
|
||||
type = AppCommandOptionType.channel.value
|
||||
for channel_id, channel_data in resolved.get('channels', {}).items():
|
||||
key = ResolveKey(id=channel_id, type=type)
|
||||
if channel_data['type'] in (10, 11, 12):
|
||||
# The guild ID can't be none in this case
|
||||
completed[key] = AppCommandThread(state=state, data=channel_data, guild_id=guild_id) # type: ignore
|
||||
else:
|
||||
# The guild ID can't be none in this case
|
||||
completed[key] = AppCommandChannel(state=state, data=channel_data, guild_id=guild_id) # type: ignore
|
||||
|
||||
type = AppCommandOptionType.attachment.value
|
||||
completed.update(
|
||||
{
|
||||
ResolveKey(id=attachment_id, type=type): Attachment(data=attachment_data, state=state)
|
||||
for attachment_id, attachment_data in resolved.get('attachments', {}).items()
|
||||
}
|
||||
)
|
||||
|
||||
for message_id, message_data in resolved.get('messages', {}).items():
|
||||
channel_id = int(message_data['channel_id'])
|
||||
if guild is None:
|
||||
channel = PartialMessageable(state=state, guild_id=guild_id, id=channel_id)
|
||||
else:
|
||||
channel = guild.get_channel_or_thread(channel_id) or PartialMessageable(
|
||||
state=state, guild_id=guild_id, id=channel_id
|
||||
)
|
||||
|
||||
# Type checker doesn't understand this due to failure to narrow
|
||||
message = Message(state=state, channel=channel, data=message_data) # type: ignore
|
||||
message.guild = guild
|
||||
key = ResolveKey(id=message_id, type=-1)
|
||||
completed[key] = message
|
||||
|
||||
return completed
|
||||
|
||||
def __repr__(self) -> str:
|
||||
items = (f'{k}={v!r}' for k, v in self.__dict__.items())
|
||||
return '<{} {}>'.format(self.__class__.__name__, ' '.join(items))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(self, Namespace) and isinstance(other, Namespace):
|
||||
return self.__dict__ == other.__dict__
|
||||
return NotImplemented
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.__dict__[key]
|
||||
|
||||
def __contains__(self, key: str) -> Any:
|
||||
return key in self.__dict__
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return None
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
yield from self.__dict__.items()
|
||||
|
||||
def _update_with_defaults(self, defaults: Iterable[Tuple[str, Any]]) -> None:
|
||||
for key, value in defaults:
|
||||
self.__dict__.setdefault(key, value)
|
||||
@@ -0,0 +1,880 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Generic,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .errors import AppCommandError, TransformerError
|
||||
from .models import AppCommandChannel, AppCommandThread, Choice
|
||||
from .translator import TranslationContextLocation, TranslationContext, Translator, locale_str
|
||||
from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel, ForumChannel
|
||||
from ..abc import GuildChannel
|
||||
from ..threads import Thread
|
||||
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale
|
||||
from ..utils import MISSING, maybe_coroutine, _human_join
|
||||
from ..user import User
|
||||
from ..role import Role
|
||||
from ..member import Member
|
||||
from ..message import Attachment
|
||||
from .._types import ClientT
|
||||
|
||||
__all__ = (
|
||||
'Transformer',
|
||||
'Transform',
|
||||
'Range',
|
||||
)
|
||||
|
||||
T = TypeVar('T')
|
||||
FuncT = TypeVar('FuncT', bound=Callable[..., Any])
|
||||
ChoiceT = TypeVar('ChoiceT', str, int, float, Union[str, int, float])
|
||||
NoneType = type(None)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from .commands import Parameter
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandParameter:
|
||||
# The name of the parameter is *always* the parameter name in the code
|
||||
# Therefore, it can't be Union[str, locale_str]
|
||||
name: str = MISSING
|
||||
description: Union[str, locale_str] = MISSING
|
||||
required: bool = MISSING
|
||||
default: Any = MISSING
|
||||
choices: List[Choice[Union[str, int, float]]] = MISSING
|
||||
type: AppCommandOptionType = MISSING
|
||||
channel_types: List[ChannelType] = MISSING
|
||||
min_value: Optional[Union[int, float]] = None
|
||||
max_value: Optional[Union[int, float]] = None
|
||||
autocomplete: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None
|
||||
_rename: Union[str, locale_str] = MISSING
|
||||
_annotation: Any = MISSING
|
||||
|
||||
async def get_translated_payload(self, translator: Translator, data: Parameter) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
|
||||
rename = self._rename
|
||||
description = self.description
|
||||
needs_name_translations = isinstance(rename, locale_str)
|
||||
needs_description_translations = isinstance(description, locale_str)
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
|
||||
# Prevent creating these objects in a heavy loop
|
||||
name_context = TranslationContext(location=TranslationContextLocation.parameter_name, data=data)
|
||||
description_context = TranslationContext(location=TranslationContextLocation.parameter_description, data=data)
|
||||
for locale in Locale:
|
||||
if needs_name_translations:
|
||||
translation = await translator._checked_translate(rename, locale, name_context)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
if needs_description_translations:
|
||||
translation = await translator._checked_translate(description, locale, description_context)
|
||||
if translation is not None:
|
||||
description_localizations[locale.value] = translation
|
||||
|
||||
if self.choices:
|
||||
base['choices'] = [await choice.get_translated_payload(translator) for choice in self.choices]
|
||||
|
||||
if name_localizations:
|
||||
base['name_localizations'] = name_localizations
|
||||
|
||||
if description_localizations:
|
||||
base['description_localizations'] = description_localizations
|
||||
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
'name': self.display_name,
|
||||
'description': str(self.description),
|
||||
'required': self.required,
|
||||
}
|
||||
|
||||
if self.choices:
|
||||
base['choices'] = [choice.to_dict() for choice in self.choices]
|
||||
if self.channel_types:
|
||||
base['channel_types'] = [t.value for t in self.channel_types]
|
||||
if self.autocomplete:
|
||||
base['autocomplete'] = True
|
||||
|
||||
min_key, max_key = (
|
||||
('min_value', 'max_value') if self.type is not AppCommandOptionType.string else ('min_length', 'max_length')
|
||||
)
|
||||
if self.min_value is not None:
|
||||
base[min_key] = self.min_value
|
||||
if self.max_value is not None:
|
||||
base[max_key] = self.max_value
|
||||
|
||||
return base
|
||||
|
||||
def _convert_to_locale_strings(self) -> None:
|
||||
if self._rename is MISSING:
|
||||
self._rename = locale_str(self.name)
|
||||
elif isinstance(self._rename, str):
|
||||
self._rename = locale_str(self._rename)
|
||||
|
||||
if isinstance(self.description, str):
|
||||
self.description = locale_str(self.description)
|
||||
|
||||
if self.choices:
|
||||
for choice in self.choices:
|
||||
if choice._locale_name is None:
|
||||
choice._locale_name = locale_str(choice.name)
|
||||
|
||||
def is_choice_annotation(self) -> bool:
|
||||
return getattr(self._annotation, '__discord_app_commands_is_choice__', False)
|
||||
|
||||
async def transform(self, interaction: Interaction, value: Any, /) -> Any:
|
||||
if hasattr(self._annotation, '__discord_app_commands_transformer__'):
|
||||
# This one needs special handling for type safety reasons
|
||||
if self._annotation.__discord_app_commands_is_choice__:
|
||||
choice = next((c for c in self.choices if c.value == value), None)
|
||||
if choice is None:
|
||||
raise TransformerError(value, self.type, self._annotation)
|
||||
return choice
|
||||
|
||||
try:
|
||||
return await maybe_coroutine(self._annotation.transform, interaction, value)
|
||||
except AppCommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise TransformerError(value, self.type, self._annotation) from e
|
||||
|
||||
return value
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
""":class:`str`: The name of the parameter as it should be displayed to the user."""
|
||||
return self.name if self._rename is MISSING else str(self._rename)
|
||||
|
||||
|
||||
class Transformer(Generic[ClientT]):
|
||||
"""The base class that allows a type annotation in an application command parameter
|
||||
to map into a :class:`~discord.AppCommandOptionType` and transform the raw value into one
|
||||
from this type.
|
||||
|
||||
This class is customisable through the overriding of methods and properties in the class
|
||||
and by using it as the second type parameter of the :class:`~discord.app_commands.Transform`
|
||||
class. For example, to convert a string into a custom pair type:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
class Point(typing.NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
class PointTransformer(app_commands.Transformer):
|
||||
async def transform(self, interaction: discord.Interaction, value: str) -> Point:
|
||||
(x, _, y) = value.partition(',')
|
||||
return Point(x=int(x.strip()), y=int(y.strip()))
|
||||
|
||||
@app_commands.command()
|
||||
async def graph(
|
||||
interaction: discord.Interaction,
|
||||
point: app_commands.Transform[Point, PointTransformer],
|
||||
):
|
||||
await interaction.response.send_message(str(point))
|
||||
|
||||
If a class is passed instead of an instance to the second type parameter, then it is
|
||||
constructed with no arguments passed to the ``__init__`` method.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__discord_app_commands_transformer__: ClassVar[bool] = True
|
||||
__discord_app_commands_is_choice__: ClassVar[bool] = False
|
||||
|
||||
# This is needed to pass typing's type checks.
|
||||
# e.g. Optional[MyTransformer]
|
||||
def __call__(self) -> None:
|
||||
pass
|
||||
|
||||
def __or__(self, rhs: Any) -> Any:
|
||||
return Union[self, rhs]
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
""":class:`~discord.AppCommandOptionType`: The option type associated with this transformer.
|
||||
|
||||
This must be a :obj:`property`.
|
||||
|
||||
Defaults to :attr:`~discord.AppCommandOptionType.string`.
|
||||
"""
|
||||
return AppCommandOptionType.string
|
||||
|
||||
@property
|
||||
def channel_types(self) -> List[ChannelType]:
|
||||
"""List[:class:`~discord.ChannelType`]: A list of channel types that are allowed to this parameter.
|
||||
|
||||
Only valid if the :meth:`type` returns :attr:`~discord.AppCommandOptionType.channel`.
|
||||
|
||||
This must be a :obj:`property`.
|
||||
|
||||
Defaults to an empty list.
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def min_value(self) -> Optional[Union[int, float]]:
|
||||
"""Optional[:class:`int`]: The minimum supported value for this parameter.
|
||||
|
||||
Only valid if the :meth:`type` returns :attr:`~discord.AppCommandOptionType.number`
|
||||
:attr:`~discord.AppCommandOptionType.integer`, or :attr:`~discord.AppCommandOptionType.string`.
|
||||
|
||||
This must be a :obj:`property`.
|
||||
|
||||
Defaults to ``None``.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def max_value(self) -> Optional[Union[int, float]]:
|
||||
"""Optional[:class:`int`]: The maximum supported value for this parameter.
|
||||
|
||||
Only valid if the :meth:`type` returns :attr:`~discord.AppCommandOptionType.number`
|
||||
:attr:`~discord.AppCommandOptionType.integer`, or :attr:`~discord.AppCommandOptionType.string`.
|
||||
|
||||
This must be a :obj:`property`.
|
||||
|
||||
Defaults to ``None``.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def choices(self) -> Optional[List[Choice[Union[int, float, str]]]]:
|
||||
"""Optional[List[:class:`~discord.app_commands.Choice`]]: A list of up to 25 choices that are allowed to this parameter.
|
||||
|
||||
Only valid if the :meth:`type` returns :attr:`~discord.AppCommandOptionType.number`
|
||||
:attr:`~discord.AppCommandOptionType.integer`, or :attr:`~discord.AppCommandOptionType.string`.
|
||||
|
||||
This must be a :obj:`property`.
|
||||
|
||||
Defaults to ``None``.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def _error_display_name(self) -> str:
|
||||
name = self.__class__.__name__
|
||||
if name.endswith('Transformer'):
|
||||
return name[:-11]
|
||||
else:
|
||||
return name
|
||||
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any:
|
||||
"""|maybecoro|
|
||||
|
||||
Transforms the converted option value into another value.
|
||||
|
||||
The value passed into this transform function is the same as the
|
||||
one in the :class:`conversion table <discord.app_commands.Namespace>`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction being handled.
|
||||
value: Any
|
||||
The value of the given argument after being resolved.
|
||||
See the :class:`conversion table <discord.app_commands.Namespace>`
|
||||
for how certain option types correspond to certain values.
|
||||
"""
|
||||
raise NotImplementedError('Derived classes need to implement this.')
|
||||
|
||||
async def autocomplete(
|
||||
self, interaction: Interaction[ClientT], value: Union[int, float, str], /
|
||||
) -> List[Choice[Union[int, float, str]]]:
|
||||
"""|coro|
|
||||
|
||||
An autocomplete prompt handler to be automatically used by options using this transformer.
|
||||
|
||||
.. note::
|
||||
|
||||
Autocomplete is only supported for options with a :meth:`~discord.app_commands.Transformer.type`
|
||||
of :attr:`~discord.AppCommandOptionType.string`, :attr:`~discord.AppCommandOptionType.integer`,
|
||||
or :attr:`~discord.AppCommandOptionType.number`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The autocomplete interaction being handled.
|
||||
value: Union[:class:`str`, :class:`int`, :class:`float`]
|
||||
The current value entered by the user.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`~discord.app_commands.Choice`]
|
||||
A list of choices to be displayed to the user, a maximum of 25.
|
||||
|
||||
"""
|
||||
raise NotImplementedError('Derived classes can implement this.')
|
||||
|
||||
|
||||
class IdentityTransformer(Transformer[ClientT]):
|
||||
def __init__(self, type: AppCommandOptionType) -> None:
|
||||
self._type = type
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return self._type
|
||||
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
class RangeTransformer(IdentityTransformer):
|
||||
def __init__(
|
||||
self,
|
||||
opt_type: AppCommandOptionType,
|
||||
*,
|
||||
min: Optional[Union[int, float]] = None,
|
||||
max: Optional[Union[int, float]] = None,
|
||||
) -> None:
|
||||
if min and max and min > max:
|
||||
raise TypeError('minimum cannot be larger than maximum')
|
||||
|
||||
self._min: Optional[Union[int, float]] = min
|
||||
self._max: Optional[Union[int, float]] = max
|
||||
super().__init__(opt_type)
|
||||
|
||||
@property
|
||||
def min_value(self) -> Optional[Union[int, float]]:
|
||||
return self._min
|
||||
|
||||
@property
|
||||
def max_value(self) -> Optional[Union[int, float]]:
|
||||
return self._max
|
||||
|
||||
|
||||
class LiteralTransformer(IdentityTransformer):
|
||||
def __init__(self, values: Tuple[Any, ...]) -> None:
|
||||
first = type(values[0])
|
||||
if first is int:
|
||||
opt_type = AppCommandOptionType.integer
|
||||
elif first is float:
|
||||
opt_type = AppCommandOptionType.number
|
||||
elif first is str:
|
||||
opt_type = AppCommandOptionType.string
|
||||
else:
|
||||
raise TypeError(f'expected int, str, or float values not {first!r}')
|
||||
|
||||
self._choices = [Choice(name=str(v), value=v) for v in values]
|
||||
super().__init__(opt_type)
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self._choices
|
||||
|
||||
|
||||
class ChoiceTransformer(IdentityTransformer):
|
||||
__discord_app_commands_is_choice__: ClassVar[bool] = True
|
||||
|
||||
def __init__(self, inner_type: Any) -> None:
|
||||
if inner_type is int:
|
||||
opt_type = AppCommandOptionType.integer
|
||||
elif inner_type is float:
|
||||
opt_type = AppCommandOptionType.number
|
||||
elif inner_type is str:
|
||||
opt_type = AppCommandOptionType.string
|
||||
else:
|
||||
raise TypeError(f'expected int, str, or float values not {inner_type!r}')
|
||||
|
||||
super().__init__(opt_type)
|
||||
|
||||
|
||||
class EnumValueTransformer(Transformer):
|
||||
def __init__(self, enum: Any) -> None:
|
||||
super().__init__()
|
||||
|
||||
values = list(enum)
|
||||
if len(values) < 2:
|
||||
raise TypeError('enum.Enum requires at least two values.')
|
||||
|
||||
first = type(values[0].value)
|
||||
if first is int:
|
||||
opt_type = AppCommandOptionType.integer
|
||||
elif first is float:
|
||||
opt_type = AppCommandOptionType.number
|
||||
elif first is str:
|
||||
opt_type = AppCommandOptionType.string
|
||||
else:
|
||||
raise TypeError(f'expected int, str, or float values not {first!r}')
|
||||
|
||||
self._type: AppCommandOptionType = opt_type
|
||||
self._enum: Any = enum
|
||||
self._choices = [Choice(name=v.name, value=v.value) for v in values]
|
||||
|
||||
@property
|
||||
def _error_display_name(self) -> str:
|
||||
return self._enum.__name__
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self._choices
|
||||
|
||||
async def transform(self, interaction: Interaction, value: Any, /) -> Any:
|
||||
return self._enum(value)
|
||||
|
||||
|
||||
class EnumNameTransformer(Transformer):
|
||||
def __init__(self, enum: Any) -> None:
|
||||
super().__init__()
|
||||
|
||||
values = list(enum)
|
||||
if len(values) < 2:
|
||||
raise TypeError('enum.Enum requires at least two values.')
|
||||
|
||||
self._enum: Any = enum
|
||||
self._choices = [Choice(name=v.name, value=v.name) for v in values]
|
||||
|
||||
@property
|
||||
def _error_display_name(self) -> str:
|
||||
return self._enum.__name__
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return AppCommandOptionType.string
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self._choices
|
||||
|
||||
async def transform(self, interaction: Interaction, value: Any, /) -> Any:
|
||||
return self._enum[value]
|
||||
|
||||
|
||||
class InlineTransformer(Transformer[ClientT]):
|
||||
def __init__(self, annotation: Any) -> None:
|
||||
super().__init__()
|
||||
self.annotation: Any = annotation
|
||||
|
||||
@property
|
||||
def _error_display_name(self) -> str:
|
||||
return self.annotation.__name__
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return AppCommandOptionType.string
|
||||
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any:
|
||||
return await self.annotation.transform(interaction, value)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Annotated as Transform
|
||||
from typing_extensions import Annotated as Range
|
||||
else:
|
||||
|
||||
class Transform:
|
||||
"""A type annotation that can be applied to a parameter to customise the behaviour of
|
||||
an option type by transforming with the given :class:`Transformer`. This requires
|
||||
the usage of two generic parameters, the first one is the type you're converting to and the second
|
||||
one is the type of the :class:`Transformer` actually doing the transformation.
|
||||
|
||||
During type checking time this is equivalent to :obj:`typing.Annotated` so type checkers understand
|
||||
the intent of the code.
|
||||
|
||||
For example usage, check :class:`Transformer`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def __class_getitem__(cls, items) -> Transformer:
|
||||
if not isinstance(items, tuple):
|
||||
raise TypeError(f'expected tuple for arguments, received {items.__class__.__name__} instead')
|
||||
|
||||
if len(items) != 2:
|
||||
raise TypeError('Transform only accepts exactly two arguments')
|
||||
|
||||
_, transformer = items
|
||||
|
||||
if inspect.isclass(transformer):
|
||||
if not issubclass(transformer, Transformer):
|
||||
raise TypeError(f'second argument of Transform must be a Transformer class not {transformer!r}')
|
||||
transformer = transformer()
|
||||
elif not isinstance(transformer, Transformer):
|
||||
raise TypeError(f'second argument of Transform must be a Transformer not {transformer.__class__.__name__}')
|
||||
|
||||
return transformer
|
||||
|
||||
class Range:
|
||||
"""A type annotation that can be applied to a parameter to require a numeric or string
|
||||
type to fit within the range provided.
|
||||
|
||||
During type checking time this is equivalent to :obj:`typing.Annotated` so type checkers understand
|
||||
the intent of the code.
|
||||
|
||||
Some example ranges:
|
||||
|
||||
- ``Range[int, 10]`` means the minimum is 10 with no maximum.
|
||||
- ``Range[int, None, 10]`` means the maximum is 10 with no minimum.
|
||||
- ``Range[int, 1, 10]`` means the minimum is 1 and the maximum is 10.
|
||||
- ``Range[float, 1.0, 5.0]`` means the minimum is 1.0 and the maximum is 5.0.
|
||||
- ``Range[str, 1, 10]`` means the minimum length is 1 and the maximum length is 10.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Examples
|
||||
----------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
async def range(interaction: discord.Interaction, value: app_commands.Range[int, 10, 12]):
|
||||
await interaction.response.send_message(f'Your value is {value}', ephemeral=True)
|
||||
"""
|
||||
|
||||
def __class_getitem__(cls, obj) -> RangeTransformer:
|
||||
if not isinstance(obj, tuple):
|
||||
raise TypeError(f'expected tuple for arguments, received {obj.__class__.__name__} instead')
|
||||
|
||||
if len(obj) == 2:
|
||||
obj = (*obj, None)
|
||||
elif len(obj) != 3:
|
||||
raise TypeError('Range accepts either two or three arguments with the first being the type of range.')
|
||||
|
||||
obj_type, min, max = obj
|
||||
|
||||
if min is None and max is None:
|
||||
raise TypeError('Range must not be empty')
|
||||
|
||||
if min is not None and max is not None:
|
||||
# At this point max and min are both not none
|
||||
if type(min) != type(max):
|
||||
raise TypeError('Both min and max in Range must be the same type')
|
||||
|
||||
if obj_type is int:
|
||||
opt_type = AppCommandOptionType.integer
|
||||
elif obj_type is float:
|
||||
opt_type = AppCommandOptionType.number
|
||||
elif obj_type is str:
|
||||
opt_type = AppCommandOptionType.string
|
||||
else:
|
||||
raise TypeError(f'expected int, float, or str as range type, received {obj_type!r} instead')
|
||||
|
||||
if obj_type in (str, int):
|
||||
cast = int
|
||||
else:
|
||||
cast = float
|
||||
|
||||
transformer = RangeTransformer(
|
||||
opt_type,
|
||||
min=cast(min) if min is not None else None,
|
||||
max=cast(max) if max is not None else None,
|
||||
)
|
||||
return transformer
|
||||
|
||||
|
||||
class MemberTransformer(Transformer[ClientT]):
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return AppCommandOptionType.user
|
||||
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Member:
|
||||
if not isinstance(value, Member):
|
||||
raise TransformerError(value, self.type, self)
|
||||
return value
|
||||
|
||||
|
||||
class BaseChannelTransformer(Transformer[ClientT]):
|
||||
def __init__(self, *channel_types: Type[Any]) -> None:
|
||||
super().__init__()
|
||||
if len(channel_types) == 1:
|
||||
display_name = channel_types[0].__name__
|
||||
types = CHANNEL_TO_TYPES[channel_types[0]]
|
||||
else:
|
||||
display_name = _human_join([t.__name__ for t in channel_types])
|
||||
types = []
|
||||
|
||||
for t in channel_types:
|
||||
try:
|
||||
types.extend(CHANNEL_TO_TYPES[t])
|
||||
except KeyError:
|
||||
raise TypeError('Union type of channels must be entirely made up of channels') from None
|
||||
|
||||
self._types: Tuple[Type[Any], ...] = channel_types
|
||||
self._channel_types: List[ChannelType] = types
|
||||
self._display_name = display_name
|
||||
|
||||
@property
|
||||
def _error_display_name(self) -> str:
|
||||
return self._display_name
|
||||
|
||||
@property
|
||||
def type(self) -> AppCommandOptionType:
|
||||
return AppCommandOptionType.channel
|
||||
|
||||
@property
|
||||
def channel_types(self) -> List[ChannelType]:
|
||||
return self._channel_types
|
||||
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /):
|
||||
resolved = value.resolve()
|
||||
if resolved is None or not isinstance(resolved, self._types):
|
||||
raise TransformerError(value, AppCommandOptionType.channel, self)
|
||||
return resolved
|
||||
|
||||
|
||||
class RawChannelTransformer(BaseChannelTransformer[ClientT]):
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /):
|
||||
if not isinstance(value, self._types):
|
||||
raise TransformerError(value, AppCommandOptionType.channel, self)
|
||||
return value
|
||||
|
||||
|
||||
class UnionChannelTransformer(BaseChannelTransformer[ClientT]):
|
||||
async def transform(self, interaction: Interaction[ClientT], value: Any, /):
|
||||
if isinstance(value, self._types):
|
||||
return value
|
||||
|
||||
resolved = value.resolve()
|
||||
if resolved is None or not isinstance(resolved, self._types):
|
||||
raise TransformerError(value, AppCommandOptionType.channel, self)
|
||||
return resolved
|
||||
|
||||
|
||||
CHANNEL_TO_TYPES: Dict[Any, List[ChannelType]] = {
|
||||
AppCommandChannel: [
|
||||
ChannelType.stage_voice,
|
||||
ChannelType.voice,
|
||||
ChannelType.text,
|
||||
ChannelType.news,
|
||||
ChannelType.category,
|
||||
ChannelType.forum,
|
||||
ChannelType.media,
|
||||
],
|
||||
GuildChannel: [
|
||||
ChannelType.stage_voice,
|
||||
ChannelType.voice,
|
||||
ChannelType.text,
|
||||
ChannelType.news,
|
||||
ChannelType.category,
|
||||
ChannelType.forum,
|
||||
ChannelType.media,
|
||||
],
|
||||
AppCommandThread: [ChannelType.news_thread, ChannelType.private_thread, ChannelType.public_thread],
|
||||
Thread: [ChannelType.news_thread, ChannelType.private_thread, ChannelType.public_thread],
|
||||
StageChannel: [ChannelType.stage_voice],
|
||||
VoiceChannel: [ChannelType.voice],
|
||||
TextChannel: [ChannelType.text, ChannelType.news],
|
||||
CategoryChannel: [ChannelType.category],
|
||||
ForumChannel: [ChannelType.forum, ChannelType.media],
|
||||
}
|
||||
|
||||
BUILT_IN_TRANSFORMERS: Dict[Any, Transformer] = {
|
||||
str: IdentityTransformer(AppCommandOptionType.string),
|
||||
int: IdentityTransformer(AppCommandOptionType.integer),
|
||||
float: IdentityTransformer(AppCommandOptionType.number),
|
||||
bool: IdentityTransformer(AppCommandOptionType.boolean),
|
||||
User: IdentityTransformer(AppCommandOptionType.user),
|
||||
Member: MemberTransformer(),
|
||||
Role: IdentityTransformer(AppCommandOptionType.role),
|
||||
AppCommandChannel: RawChannelTransformer(AppCommandChannel),
|
||||
AppCommandThread: RawChannelTransformer(AppCommandThread),
|
||||
GuildChannel: BaseChannelTransformer(GuildChannel),
|
||||
Thread: BaseChannelTransformer(Thread),
|
||||
StageChannel: BaseChannelTransformer(StageChannel),
|
||||
VoiceChannel: BaseChannelTransformer(VoiceChannel),
|
||||
TextChannel: BaseChannelTransformer(TextChannel),
|
||||
CategoryChannel: BaseChannelTransformer(CategoryChannel),
|
||||
ForumChannel: BaseChannelTransformer(ForumChannel),
|
||||
Attachment: IdentityTransformer(AppCommandOptionType.attachment),
|
||||
}
|
||||
|
||||
ALLOWED_DEFAULTS: Dict[AppCommandOptionType, Tuple[Type[Any], ...]] = {
|
||||
AppCommandOptionType.string: (str, NoneType),
|
||||
AppCommandOptionType.integer: (int, NoneType),
|
||||
AppCommandOptionType.boolean: (bool, NoneType),
|
||||
AppCommandOptionType.number: (float, NoneType),
|
||||
}
|
||||
|
||||
|
||||
def get_supported_annotation(
|
||||
annotation: Any,
|
||||
*,
|
||||
_none: type = NoneType,
|
||||
_mapping: Dict[Any, Transformer] = BUILT_IN_TRANSFORMERS,
|
||||
) -> Tuple[Any, Any, bool]:
|
||||
"""Returns an appropriate, yet supported, annotation along with an optional default value.
|
||||
|
||||
The third boolean element of the tuple indicates if default values should be validated.
|
||||
|
||||
This differs from the built in mapping by supporting a few more things.
|
||||
Likewise, this returns a "transformed" annotation that is ready to use with CommandParameter.transform.
|
||||
"""
|
||||
|
||||
try:
|
||||
return (_mapping[annotation], MISSING, True)
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
if isinstance(annotation, Transformer):
|
||||
return (annotation, MISSING, False)
|
||||
|
||||
if inspect.isclass(annotation):
|
||||
if issubclass(annotation, Transformer):
|
||||
return (annotation(), MISSING, False)
|
||||
if issubclass(annotation, (Enum, InternalEnum)):
|
||||
if all(isinstance(v.value, (str, int, float)) for v in annotation):
|
||||
return (EnumValueTransformer(annotation), MISSING, False)
|
||||
else:
|
||||
return (EnumNameTransformer(annotation), MISSING, False)
|
||||
if annotation is Choice:
|
||||
raise TypeError('Choice requires a type argument of int, str, or float')
|
||||
|
||||
# Check if a transform @classmethod is given to the class
|
||||
# These flatten into simple "inline" transformers with implicit strings
|
||||
transform_classmethod = annotation.__dict__.get('transform', None)
|
||||
if isinstance(transform_classmethod, classmethod):
|
||||
params = inspect.signature(transform_classmethod.__func__).parameters
|
||||
if len(params) != 3:
|
||||
raise TypeError('Inline transformer with transform classmethod requires 3 parameters')
|
||||
if not inspect.iscoroutinefunction(transform_classmethod.__func__):
|
||||
raise TypeError('Inline transformer with transform classmethod must be a coroutine')
|
||||
return (InlineTransformer(annotation), MISSING, False)
|
||||
|
||||
# Check if there's an origin
|
||||
origin = getattr(annotation, '__origin__', None)
|
||||
if origin is Literal:
|
||||
args = annotation.__args__
|
||||
return (LiteralTransformer(args), MISSING, True)
|
||||
|
||||
if origin is Choice:
|
||||
arg = annotation.__args__[0]
|
||||
return (ChoiceTransformer(arg), MISSING, True)
|
||||
|
||||
if origin is not Union:
|
||||
# Only Union/Optional is supported right now so bail early
|
||||
raise TypeError(f'unsupported type annotation {annotation!r}')
|
||||
|
||||
default = MISSING
|
||||
args = annotation.__args__
|
||||
if args[-1] is _none:
|
||||
if len(args) == 2:
|
||||
underlying = args[0]
|
||||
inner, _, validate_default = get_supported_annotation(underlying)
|
||||
if inner is None:
|
||||
raise TypeError(f'unsupported inner optional type {underlying!r}')
|
||||
return (inner, None, validate_default)
|
||||
else:
|
||||
args = args[:-1]
|
||||
default = None
|
||||
|
||||
# Check for channel union types
|
||||
if any(arg in CHANNEL_TO_TYPES for arg in args):
|
||||
# If any channel type is given, then *all* must be channel types
|
||||
return (UnionChannelTransformer(*args), default, True)
|
||||
|
||||
# The only valid transformations here are:
|
||||
# [Member, User] => user
|
||||
# [Member, User, Role] => mentionable
|
||||
# [Member | User, Role] => mentionable
|
||||
supported_types: Set[Any] = {Role, Member, User}
|
||||
if not all(arg in supported_types for arg in args):
|
||||
raise TypeError(f'unsupported types given inside {annotation!r}')
|
||||
if args == (User, Member) or args == (Member, User):
|
||||
return (IdentityTransformer(AppCommandOptionType.user), default, True)
|
||||
|
||||
return (IdentityTransformer(AppCommandOptionType.mentionable), default, True)
|
||||
|
||||
|
||||
def annotation_to_parameter(annotation: Any, parameter: inspect.Parameter) -> CommandParameter:
|
||||
"""Returns the appropriate :class:`CommandParameter` for the given annotation.
|
||||
|
||||
The resulting ``_annotation`` attribute might not match the one given here and might
|
||||
be transformed in order to be easier to call from the ``transform`` asynchronous function
|
||||
of a command parameter.
|
||||
"""
|
||||
|
||||
(inner, default, validate_default) = get_supported_annotation(annotation)
|
||||
type = inner.type
|
||||
|
||||
if default is MISSING or default is None:
|
||||
param_default = parameter.default
|
||||
if param_default is not parameter.empty:
|
||||
default = param_default
|
||||
|
||||
# Verify validity of the default parameter
|
||||
if default is not MISSING and validate_default:
|
||||
valid_types: Tuple[Any, ...] = ALLOWED_DEFAULTS.get(type, (NoneType,))
|
||||
if not isinstance(default, valid_types):
|
||||
raise TypeError(f'invalid default parameter type given ({default.__class__}), expected {valid_types}')
|
||||
|
||||
result = CommandParameter(
|
||||
type=type,
|
||||
_annotation=inner,
|
||||
default=default,
|
||||
required=default is MISSING,
|
||||
name=parameter.name,
|
||||
)
|
||||
|
||||
choices = inner.choices
|
||||
if choices is not None:
|
||||
result.choices = choices
|
||||
|
||||
# These methods should be duck typed
|
||||
if type in (AppCommandOptionType.number, AppCommandOptionType.string, AppCommandOptionType.integer):
|
||||
result.min_value = inner.min_value
|
||||
result.max_value = inner.max_value
|
||||
|
||||
if type is AppCommandOptionType.channel:
|
||||
result.channel_types = inner.channel_types
|
||||
|
||||
if parameter.kind in (parameter.POSITIONAL_ONLY, parameter.VAR_KEYWORD, parameter.VAR_POSITIONAL):
|
||||
raise TypeError(f'unsupported parameter kind in callback: {parameter.kind!s}')
|
||||
|
||||
# Check if the method is overridden
|
||||
if inner.autocomplete.__func__ is not Transformer.autocomplete:
|
||||
from .commands import validate_auto_complete_callback
|
||||
|
||||
result.autocomplete = validate_auto_complete_callback(inner.autocomplete)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, Generic, Literal, Optional, TypeVar, Union, overload
|
||||
from .errors import TranslationError
|
||||
from ..enums import Enum, Locale
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command, ContextMenu, Group, Parameter
|
||||
from .models import Choice
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TranslationContextLocation',
|
||||
'TranslationContextTypes',
|
||||
'TranslationContext',
|
||||
'Translator',
|
||||
'locale_str',
|
||||
)
|
||||
|
||||
|
||||
class TranslationContextLocation(Enum):
|
||||
command_name = 0
|
||||
command_description = 1
|
||||
group_name = 2
|
||||
group_description = 3
|
||||
parameter_name = 4
|
||||
parameter_description = 5
|
||||
choice_name = 6
|
||||
other = 7
|
||||
|
||||
|
||||
_L = TypeVar('_L', bound=TranslationContextLocation)
|
||||
_D = TypeVar('_D')
|
||||
|
||||
|
||||
class TranslationContext(Generic[_L, _D]):
|
||||
"""A class that provides context for the :class:`locale_str` being translated.
|
||||
|
||||
This is useful to determine where exactly the string is located and aid in looking
|
||||
up the actual translation.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
location: :class:`TranslationContextLocation`
|
||||
The location where this string is located.
|
||||
data: Any
|
||||
The extraneous data that is being translated.
|
||||
"""
|
||||
|
||||
__slots__ = ('location', 'data')
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, location: Literal[TranslationContextLocation.command_name], data: Union[Command[Any, ..., Any], ContextMenu]
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, location: Literal[TranslationContextLocation.command_description], data: Command[Any, ..., Any]
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
location: Literal[TranslationContextLocation.group_name, TranslationContextLocation.group_description],
|
||||
data: Group,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
location: Literal[TranslationContextLocation.parameter_name, TranslationContextLocation.parameter_description],
|
||||
data: Parameter,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, location: Literal[TranslationContextLocation.choice_name], data: Choice[Any]) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, location: Literal[TranslationContextLocation.other], data: Any) -> None: ...
|
||||
|
||||
def __init__(self, location: _L, data: _D) -> None: # type: ignore # pyright doesn't like the overloads
|
||||
self.location: _L = location
|
||||
self.data: _D = data
|
||||
|
||||
|
||||
# For type checking purposes, it makes sense to allow the user to leverage type narrowing
|
||||
# So code like this works as expected:
|
||||
#
|
||||
# if context.type == TranslationContextLocation.command_name:
|
||||
# reveal_type(context.data) # Revealed type is Command | ContextMenu
|
||||
#
|
||||
# This requires a union of types
|
||||
CommandNameTranslationContext = TranslationContext[
|
||||
Literal[TranslationContextLocation.command_name], Union['Command[Any, ..., Any]', 'ContextMenu']
|
||||
]
|
||||
CommandDescriptionTranslationContext = TranslationContext[
|
||||
Literal[TranslationContextLocation.command_description], 'Command[Any, ..., Any]'
|
||||
]
|
||||
GroupTranslationContext = TranslationContext[
|
||||
Literal[TranslationContextLocation.group_name, TranslationContextLocation.group_description], 'Group'
|
||||
]
|
||||
ParameterTranslationContext = TranslationContext[
|
||||
Literal[TranslationContextLocation.parameter_name, TranslationContextLocation.parameter_description], 'Parameter'
|
||||
]
|
||||
ChoiceTranslationContext = TranslationContext[Literal[TranslationContextLocation.choice_name], 'Choice[Any]']
|
||||
OtherTranslationContext = TranslationContext[Literal[TranslationContextLocation.other], Any]
|
||||
|
||||
TranslationContextTypes = Union[
|
||||
CommandNameTranslationContext,
|
||||
CommandDescriptionTranslationContext,
|
||||
GroupTranslationContext,
|
||||
ParameterTranslationContext,
|
||||
ChoiceTranslationContext,
|
||||
OtherTranslationContext,
|
||||
]
|
||||
|
||||
|
||||
class Translator:
|
||||
"""A class that handles translations for commands, parameters, and choices.
|
||||
|
||||
Translations are done lazily in order to allow for async enabled translations as well
|
||||
as supporting a wide array of translation systems such as :mod:`gettext` and
|
||||
`Project Fluent <https://projectfluent.org>`_.
|
||||
|
||||
In order for a translator to be used, it must be set using the :meth:`CommandTree.set_translator`
|
||||
method. The translation flow for a string is as follows:
|
||||
|
||||
1. Use :class:`locale_str` instead of :class:`str` in areas of a command you want to be translated.
|
||||
- Currently, these are command names, command descriptions, parameter names, parameter descriptions, and choice names.
|
||||
- This can also be used inside the :func:`~discord.app_commands.describe` decorator.
|
||||
2. Call :meth:`CommandTree.set_translator` to the translator instance that will handle the translations.
|
||||
3. Call :meth:`CommandTree.sync`
|
||||
4. The library will call :meth:`Translator.translate` on all the relevant strings being translated.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
async def load(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
An asynchronous setup function for loading the translation system.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
This is invoked when :meth:`CommandTree.set_translator` is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def unload(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
An asynchronous teardown function for unloading the translation system.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
This is invoked when :meth:`CommandTree.set_translator` is called
|
||||
if a tree already has a translator or when :meth:`discord.Client.close` is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _checked_translate(
|
||||
self, string: locale_str, locale: Locale, context: TranslationContextTypes
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
return await self.translate(string, locale, context)
|
||||
except TranslationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise TranslationError(string=string, locale=locale, context=context) from e
|
||||
|
||||
async def translate(self, string: locale_str, locale: Locale, context: TranslationContextTypes) -> Optional[str]:
|
||||
"""|coro|
|
||||
|
||||
Translates the given string to the specified locale.
|
||||
|
||||
If the string cannot be translated, ``None`` should be returned.
|
||||
|
||||
The default implementation returns ``None``.
|
||||
|
||||
If an exception is raised in this method, it should inherit from :exc:`TranslationError`.
|
||||
If it doesn't, then when this is called the exception will be chained with it instead.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
string: :class:`locale_str`
|
||||
The string being translated.
|
||||
locale: :class:`~discord.Locale`
|
||||
The locale being requested for translation.
|
||||
context: :class:`TranslationContext`
|
||||
The translation context where the string originated from.
|
||||
For better type checking ergonomics, the ``TranslationContextTypes``
|
||||
type can be used instead to aid with type narrowing. It is functionally
|
||||
equivalent to :class:`TranslationContext`.
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class locale_str:
|
||||
"""Marks a string as ready for translation.
|
||||
|
||||
This is done lazily and is not actually translated until :meth:`CommandTree.sync` is called.
|
||||
|
||||
The sync method then ultimately defers the responsibility of translating to the :class:`Translator`
|
||||
instance used by the :class:`CommandTree`. For more information on the translation flow, see the
|
||||
:class:`Translator` documentation.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the message passed to the string.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the string is equal to another string.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the string is not equal to another string.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the string.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
message: :class:`str`
|
||||
The message being translated. Once set, this cannot be changed.
|
||||
|
||||
.. warning::
|
||||
|
||||
This must be the default "message" that you send to Discord.
|
||||
Discord sends this message back to the library and the library
|
||||
uses it to access the data in order to dispatch commands.
|
||||
|
||||
For example, in a command name context, if the command
|
||||
name is ``foo`` then the message *must* also be ``foo``.
|
||||
For other translation systems that require a message ID such
|
||||
as Fluent, consider using a keyword argument to pass it in.
|
||||
extras: :class:`dict`
|
||||
A dict of user provided extras to attach to the translated string.
|
||||
This can be used to add more context, information, or any metadata necessary
|
||||
to aid in actually translating the string.
|
||||
|
||||
Since these are passed via keyword arguments, the keys are strings.
|
||||
"""
|
||||
|
||||
__slots__ = ('__message', 'extras')
|
||||
|
||||
def __init__(self, message: str, /, **kwargs: Any) -> None:
|
||||
self.__message: str = message
|
||||
self.extras: dict[str, Any] = kwargs
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.__message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__message
|
||||
|
||||
def __repr__(self) -> str:
|
||||
kwargs = ', '.join(f'{k}={v!r}' for k, v in self.extras.items())
|
||||
if kwargs:
|
||||
return f'{self.__class__.__name__}({self.__message!r}, {kwargs})'
|
||||
return f'{self.__class__.__name__}({self.__message!r})'
|
||||
|
||||
def __eq__(self, obj: object) -> bool:
|
||||
return isinstance(obj, locale_str) and self.message == obj.message
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.__message)
|
||||
1304
venv/lib/python3.12/site-packages/discord/app_commands/tree.py
Normal file
1304
venv/lib/python3.12/site-packages/discord/app_commands/tree.py
Normal file
File diff suppressed because it is too large
Load Diff
645
venv/lib/python3.12/site-packages/discord/appinfo.py
Normal file
645
venv/lib/python3.12/site-packages/discord/appinfo.py
Normal file
@@ -0,0 +1,645 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 List, TYPE_CHECKING, Literal, Optional
|
||||
|
||||
from . import utils
|
||||
from .asset import Asset
|
||||
from .flags import ApplicationFlags
|
||||
from .permissions import Permissions
|
||||
from .utils import MISSING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, Any
|
||||
|
||||
from .guild import Guild
|
||||
from .types.appinfo import (
|
||||
AppInfo as AppInfoPayload,
|
||||
PartialAppInfo as PartialAppInfoPayload,
|
||||
Team as TeamPayload,
|
||||
InstallParams as InstallParamsPayload,
|
||||
AppIntegrationTypeConfig as AppIntegrationTypeConfigPayload,
|
||||
)
|
||||
from .user import User
|
||||
from .state import ConnectionState
|
||||
|
||||
__all__ = (
|
||||
'AppInfo',
|
||||
'PartialAppInfo',
|
||||
'AppInstallParams',
|
||||
'IntegrationTypeConfig',
|
||||
)
|
||||
|
||||
|
||||
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 and the
|
||||
GameSDK's :ddocs:`GetTicket <game-sdk/applications#getticket>`.
|
||||
|
||||
.. 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
|
||||
|
||||
tags: List[:class:`str`]
|
||||
The list of tags describing the functionality of the application.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
custom_install_url: List[:class:`str`]
|
||||
The custom authorization URL for the application, if enabled.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
install_params: Optional[:class:`AppInstallParams`]
|
||||
The settings for custom authorization URL of application, if enabled.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
role_connections_verification_url: Optional[:class:`str`]
|
||||
The application's connection verification URL which will render the application as
|
||||
a verification method in the guild's role verification configuration.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
interactions_endpoint_url: Optional[:class:`str`]
|
||||
The interactions endpoint url of the application to receive interactions over this endpoint rather than
|
||||
over the gateway, if configured.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
redirect_uris: List[:class:`str`]
|
||||
A list of authentication redirect URIs.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
approximate_guild_count: :class:`int`
|
||||
The approximate count of the guilds the bot was added to.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
approximate_user_install_count: Optional[:class:`int`]
|
||||
The approximate count of the user-level installations the bot has.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_state',
|
||||
'description',
|
||||
'id',
|
||||
'name',
|
||||
'rpc_origins',
|
||||
'bot_public',
|
||||
'bot_require_code_grant',
|
||||
'owner',
|
||||
'_icon',
|
||||
'verify_key',
|
||||
'team',
|
||||
'guild_id',
|
||||
'primary_sku_id',
|
||||
'slug',
|
||||
'_cover_image',
|
||||
'_flags',
|
||||
'terms_of_service_url',
|
||||
'privacy_policy_url',
|
||||
'tags',
|
||||
'custom_install_url',
|
||||
'install_params',
|
||||
'role_connections_verification_url',
|
||||
'interactions_endpoint_url',
|
||||
'redirect_uris',
|
||||
'approximate_guild_count',
|
||||
'approximate_user_install_count',
|
||||
'_integration_types_config',
|
||||
)
|
||||
|
||||
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: Optional[str] = data['icon']
|
||||
self.rpc_origins: Optional[List[str]] = data.get('rpc_origins')
|
||||
self.bot_public: bool = data['bot_public']
|
||||
self.bot_require_code_grant: bool = data['bot_require_code_grant']
|
||||
self.owner: User = state.create_user(data['owner'])
|
||||
|
||||
team: Optional[TeamPayload] = data.get('team')
|
||||
self.team: Optional[Team] = Team(state, team) if team else None
|
||||
|
||||
self.verify_key: str = data['verify_key']
|
||||
|
||||
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
|
||||
|
||||
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, 'primary_sku_id')
|
||||
self.slug: Optional[str] = data.get('slug')
|
||||
self._flags: int = data.get('flags', 0)
|
||||
self._cover_image: Optional[str] = data.get('cover_image')
|
||||
self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url')
|
||||
self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url')
|
||||
self.tags: List[str] = data.get('tags', [])
|
||||
self.custom_install_url: Optional[str] = data.get('custom_install_url')
|
||||
self.role_connections_verification_url: Optional[str] = data.get('role_connections_verification_url')
|
||||
|
||||
params = data.get('install_params')
|
||||
self.install_params: Optional[AppInstallParams] = AppInstallParams(params) if params else None
|
||||
self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url')
|
||||
self.redirect_uris: List[str] = data.get('redirect_uris', [])
|
||||
self.approximate_guild_count: int = data.get('approximate_guild_count', 0)
|
||||
self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count')
|
||||
self._integration_types_config: Dict[Literal['0', '1'], AppIntegrationTypeConfigPayload] = 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 icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_icon(self._state, self.id, self._icon, path='app')
|
||||
|
||||
@property
|
||||
def cover_image(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the 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) -> Optional[Guild]:
|
||||
"""Optional[:class:`Guild`]: 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 flags(self) -> ApplicationFlags:
|
||||
""":class:`ApplicationFlags`: The application's flags.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return ApplicationFlags._from_value(self._flags)
|
||||
|
||||
@property
|
||||
def guild_integration_config(self) -> Optional[IntegrationTypeConfig]:
|
||||
"""Optional[:class:`IntegrationTypeConfig`]: The default settings for the
|
||||
application's installation context in a guild.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
"""
|
||||
if not self._integration_types_config:
|
||||
return None
|
||||
|
||||
try:
|
||||
return IntegrationTypeConfig(self._integration_types_config['0'])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def user_integration_config(self) -> Optional[IntegrationTypeConfig]:
|
||||
"""Optional[:class:`IntegrationTypeConfig`]: The default settings for the
|
||||
application's installation context as a user.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
"""
|
||||
if not self._integration_types_config:
|
||||
return None
|
||||
|
||||
try:
|
||||
return IntegrationTypeConfig(self._integration_types_config['1'])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
reason: Optional[str] = MISSING,
|
||||
custom_install_url: Optional[str] = MISSING,
|
||||
description: Optional[str] = MISSING,
|
||||
role_connections_verification_url: Optional[str] = MISSING,
|
||||
install_params_scopes: Optional[List[str]] = MISSING,
|
||||
install_params_permissions: Optional[Permissions] = MISSING,
|
||||
flags: Optional[ApplicationFlags] = MISSING,
|
||||
icon: Optional[bytes] = MISSING,
|
||||
cover_image: Optional[bytes] = MISSING,
|
||||
interactions_endpoint_url: Optional[str] = MISSING,
|
||||
tags: Optional[List[str]] = MISSING,
|
||||
guild_install_scopes: Optional[List[str]] = MISSING,
|
||||
guild_install_permissions: Optional[Permissions] = MISSING,
|
||||
user_install_scopes: Optional[List[str]] = MISSING,
|
||||
user_install_permissions: Optional[Permissions] = MISSING,
|
||||
) -> AppInfo:
|
||||
r"""|coro|
|
||||
|
||||
Edits the application info.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_install_url: Optional[:class:`str`]
|
||||
The new custom authorization URL for the application. Can be ``None`` to remove the URL.
|
||||
description: Optional[:class:`str`]
|
||||
The new application description. Can be ``None`` to remove the description.
|
||||
role_connections_verification_url: Optional[:class:`str`]
|
||||
The new application’s connection verification URL which will render the application
|
||||
as a verification method in the guild’s role verification configuration. Can be ``None`` to remove the URL.
|
||||
install_params_scopes: Optional[List[:class:`str`]]
|
||||
The new list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>` of
|
||||
the :attr:`~install_params`. Can be ``None`` to remove the scopes.
|
||||
install_params_permissions: Optional[:class:`Permissions`]
|
||||
The new permissions of the :attr:`~install_params`. Can be ``None`` to remove the permissions.
|
||||
flags: Optional[:class:`ApplicationFlags`]
|
||||
The new application’s flags. Only limited intent flags (:attr:`~ApplicationFlags.gateway_presence_limited`,
|
||||
:attr:`~ApplicationFlags.gateway_guild_members_limited`, :attr:`~ApplicationFlags.gateway_message_content_limited`)
|
||||
can be edited. Can be ``None`` to remove the flags.
|
||||
|
||||
.. warning::
|
||||
|
||||
Editing the limited intent flags leads to the termination of the bot.
|
||||
|
||||
icon: Optional[:class:`bytes`]
|
||||
The new application’s icon as a :term:`py:bytes-like object`. Can be ``None`` to remove the icon.
|
||||
cover_image: Optional[:class:`bytes`]
|
||||
The new application’s cover image as a :term:`py:bytes-like object` on a store embed.
|
||||
The cover image is only available if the application is a game sold on Discord.
|
||||
Can be ``None`` to remove the image.
|
||||
interactions_endpoint_url: Optional[:class:`str`]
|
||||
The new interactions endpoint url of the application to receive interactions over this endpoint rather than
|
||||
over the gateway. Can be ``None`` to remove the URL.
|
||||
tags: Optional[List[:class:`str`]]
|
||||
The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags.
|
||||
guild_install_scopes: Optional[List[:class:`str`]]
|
||||
The new list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>` of
|
||||
the default guild installation context. Can be ``None`` to remove the scopes.
|
||||
|
||||
.. versionadded: 2.5
|
||||
guild_install_permissions: Optional[:class:`Permissions`]
|
||||
The new permissions of the default guild installation context. Can be ``None`` to remove the permissions.
|
||||
|
||||
.. versionadded: 2.5
|
||||
user_install_scopes: Optional[List[:class:`str`]]
|
||||
The new list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>` of
|
||||
the default user installation context. Can be ``None`` to remove the scopes.
|
||||
|
||||
.. versionadded: 2.5
|
||||
user_install_permissions: Optional[:class:`Permissions`]
|
||||
The new permissions of the default user installation context. Can be ``None`` to remove the permissions.
|
||||
|
||||
.. versionadded: 2.5
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing the application. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the application failed
|
||||
ValueError
|
||||
The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised
|
||||
when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other,
|
||||
or when ``guild_install_scopes`` and ``guild_install_permissions`` are incompatible with each other.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`AppInfo`
|
||||
The newly updated application info.
|
||||
"""
|
||||
payload: Dict[str, Any] = {}
|
||||
|
||||
if custom_install_url is not MISSING:
|
||||
payload['custom_install_url'] = custom_install_url
|
||||
|
||||
if description is not MISSING:
|
||||
payload['description'] = description
|
||||
|
||||
if role_connections_verification_url is not MISSING:
|
||||
payload['role_connections_verification_url'] = role_connections_verification_url
|
||||
|
||||
if install_params_scopes is not MISSING:
|
||||
install_params: Optional[Dict[str, Any]] = {}
|
||||
if install_params_scopes is None:
|
||||
install_params = None
|
||||
else:
|
||||
if 'bot' not in install_params_scopes and install_params_permissions is not MISSING:
|
||||
raise ValueError("'bot' must be in install_params_scopes if install_params_permissions is set")
|
||||
|
||||
install_params['scopes'] = install_params_scopes
|
||||
|
||||
if install_params_permissions is MISSING:
|
||||
install_params['permissions'] = 0
|
||||
else:
|
||||
if install_params_permissions is None:
|
||||
install_params['permissions'] = 0
|
||||
else:
|
||||
install_params['permissions'] = install_params_permissions.value
|
||||
|
||||
payload['install_params'] = install_params
|
||||
|
||||
else:
|
||||
if install_params_permissions is not MISSING:
|
||||
raise ValueError('install_params_scopes must be set if install_params_permissions is set')
|
||||
|
||||
if flags is not MISSING:
|
||||
if flags is None:
|
||||
payload['flags'] = flags
|
||||
else:
|
||||
payload['flags'] = flags.value
|
||||
|
||||
if icon is not MISSING:
|
||||
if icon is None:
|
||||
payload['icon'] = icon
|
||||
else:
|
||||
payload['icon'] = utils._bytes_to_base64_data(icon)
|
||||
|
||||
if cover_image is not MISSING:
|
||||
if cover_image is None:
|
||||
payload['cover_image'] = cover_image
|
||||
else:
|
||||
payload['cover_image'] = utils._bytes_to_base64_data(cover_image)
|
||||
|
||||
if interactions_endpoint_url is not MISSING:
|
||||
payload['interactions_endpoint_url'] = interactions_endpoint_url
|
||||
|
||||
if tags is not MISSING:
|
||||
payload['tags'] = tags
|
||||
|
||||
integration_types_config: Dict[str, Any] = {}
|
||||
if guild_install_scopes is not MISSING or guild_install_permissions is not MISSING:
|
||||
guild_install_params: Optional[Dict[str, Any]] = {}
|
||||
if guild_install_scopes in (None, MISSING):
|
||||
guild_install_scopes = []
|
||||
|
||||
if 'bot' not in guild_install_scopes and guild_install_permissions is not MISSING:
|
||||
raise ValueError("'bot' must be in guild_install_scopes if guild_install_permissions is set")
|
||||
|
||||
if guild_install_permissions in (None, MISSING):
|
||||
guild_install_params['permissions'] = 0
|
||||
else:
|
||||
guild_install_params['permissions'] = guild_install_permissions.value
|
||||
|
||||
guild_install_params['scopes'] = guild_install_scopes
|
||||
|
||||
integration_types_config['0'] = {'oauth2_install_params': guild_install_params or None}
|
||||
else:
|
||||
if guild_install_permissions is not MISSING:
|
||||
raise ValueError('guild_install_scopes must be set if guild_install_permissions is set')
|
||||
|
||||
if user_install_scopes is not MISSING or user_install_permissions is not MISSING:
|
||||
user_install_params: Optional[Dict[str, Any]] = {}
|
||||
if user_install_scopes in (None, MISSING):
|
||||
user_install_scopes = []
|
||||
|
||||
if 'bot' not in user_install_scopes and user_install_permissions is not MISSING:
|
||||
raise ValueError("'bot' must be in user_install_scopes if user_install_permissions is set")
|
||||
|
||||
if user_install_permissions in (None, MISSING):
|
||||
user_install_params['permissions'] = 0
|
||||
else:
|
||||
user_install_params['permissions'] = user_install_permissions.value
|
||||
|
||||
user_install_params['scopes'] = user_install_scopes
|
||||
|
||||
integration_types_config['1'] = {'oauth2_install_params': user_install_params or None}
|
||||
else:
|
||||
if user_install_permissions is not MISSING:
|
||||
raise ValueError('user_install_scopes must be set if user_install_permissions is set')
|
||||
|
||||
if integration_types_config:
|
||||
payload['integration_types_config'] = integration_types_config
|
||||
|
||||
data = await self._state.http.edit_application_info(reason=reason, payload=payload)
|
||||
return AppInfo(data=data, state=self._state)
|
||||
|
||||
|
||||
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.
|
||||
verify_key: :class:`str`
|
||||
The hex encoded key for verification in interactions and the
|
||||
GameSDK's :ddocs:`GetTicket <game-sdk/applications#getticket>`.
|
||||
terms_of_service_url: Optional[:class:`str`]
|
||||
The application's terms of service URL, if set.
|
||||
privacy_policy_url: Optional[:class:`str`]
|
||||
The application's privacy policy URL, if set.
|
||||
approximate_guild_count: :class:`int`
|
||||
The approximate count of the guilds the bot was added to.
|
||||
|
||||
.. versionadded:: 2.3
|
||||
redirect_uris: List[:class:`str`]
|
||||
A list of authentication redirect URIs.
|
||||
|
||||
.. versionadded:: 2.3
|
||||
interactions_endpoint_url: Optional[:class:`str`]
|
||||
The interactions endpoint url of the application to receive interactions over this endpoint rather than
|
||||
over the gateway, if configured.
|
||||
|
||||
.. versionadded:: 2.3
|
||||
role_connections_verification_url: Optional[:class:`str`]
|
||||
The application's connection verification URL which will render the application as
|
||||
a verification method in the guild's role verification configuration.
|
||||
|
||||
.. versionadded:: 2.3
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_state',
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'rpc_origins',
|
||||
'verify_key',
|
||||
'terms_of_service_url',
|
||||
'privacy_policy_url',
|
||||
'_icon',
|
||||
'_flags',
|
||||
'_cover_image',
|
||||
'approximate_guild_count',
|
||||
'redirect_uris',
|
||||
'interactions_endpoint_url',
|
||||
'role_connections_verification_url',
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data['id'])
|
||||
self.name: str = data['name']
|
||||
self._icon: Optional[str] = data.get('icon')
|
||||
self._flags: int = data.get('flags', 0)
|
||||
self._cover_image: Optional[str] = data.get('cover_image')
|
||||
self.description: str = data['description']
|
||||
self.rpc_origins: Optional[List[str]] = data.get('rpc_origins')
|
||||
self.verify_key: str = data['verify_key']
|
||||
self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url')
|
||||
self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url')
|
||||
self.approximate_guild_count: int = data.get('approximate_guild_count', 0)
|
||||
self.redirect_uris: List[str] = data.get('redirect_uris', [])
|
||||
self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url')
|
||||
self.role_connections_verification_url: Optional[str] = data.get('role_connections_verification_url')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>'
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_icon(self._state, self.id, self._icon, path='app')
|
||||
|
||||
@property
|
||||
def cover_image(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the cover image of the application's default rich presence.
|
||||
|
||||
This is only available if the application is a game sold on Discord.
|
||||
|
||||
.. versionadded:: 2.3
|
||||
"""
|
||||
if self._cover_image is None:
|
||||
return None
|
||||
return Asset._from_cover_image(self._state, self.id, self._cover_image)
|
||||
|
||||
@property
|
||||
def flags(self) -> ApplicationFlags:
|
||||
""":class:`ApplicationFlags`: The application's flags.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return ApplicationFlags._from_value(self._flags)
|
||||
|
||||
|
||||
class AppInstallParams:
|
||||
"""Represents the settings for custom authorization URL of an application.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
scopes: List[:class:`str`]
|
||||
The list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>`
|
||||
to add the application to a guild with.
|
||||
permissions: :class:`Permissions`
|
||||
The permissions to give to application in the guild.
|
||||
"""
|
||||
|
||||
__slots__ = ('scopes', 'permissions')
|
||||
|
||||
def __init__(self, data: InstallParamsPayload) -> None:
|
||||
self.scopes: List[str] = data.get('scopes', [])
|
||||
self.permissions: Permissions = Permissions(int(data['permissions']))
|
||||
|
||||
|
||||
class IntegrationTypeConfig:
|
||||
"""Represents the default settings for the application's installation context.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
Attributes
|
||||
----------
|
||||
oauth2_install_params: Optional[:class:`AppInstallParams`]
|
||||
The install params for this installation context's default in-app authorization link.
|
||||
"""
|
||||
|
||||
def __init__(self, data: AppIntegrationTypeConfigPayload) -> None:
|
||||
self.oauth2_install_params: Optional[AppInstallParams] = None
|
||||
try:
|
||||
self.oauth2_install_params = AppInstallParams(data['oauth2_install_params']) # type: ignore # EAFP
|
||||
except KeyError:
|
||||
pass
|
||||
545
venv/lib/python3.12/site-packages/discord/asset.py
Normal file
545
venv/lib/python3.12/site-packages/discord/asset.py
Normal file
@@ -0,0 +1,545 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 Any, Literal, Optional, TYPE_CHECKING, Tuple, Union
|
||||
from .errors import DiscordException
|
||||
from . import utils
|
||||
from .file import File
|
||||
|
||||
import yarl
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'Asset',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .state import ConnectionState
|
||||
from .webhook.async_ import _WebhookState
|
||||
|
||||
_State = Union[ConnectionState, _WebhookState]
|
||||
|
||||
ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png']
|
||||
ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif']
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({'jpeg', 'jpg', 'webp', 'png'})
|
||||
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {'gif'}
|
||||
|
||||
|
||||
MISSING = utils.MISSING
|
||||
|
||||
|
||||
class AssetMixin:
|
||||
__slots__ = ()
|
||||
url: str
|
||||
_state: Optional[Any]
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if self._state is None:
|
||||
raise DiscordException('Invalid state (no ConnectionState provided)')
|
||||
|
||||
return await self._state.http.get_from_cdn(self.url)
|
||||
|
||||
async def save(self, fp: Union[str, bytes, os.PathLike[Any], io.BufferedIOBase], *, seek_begin: bool = True) -> int:
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
|
||||
The file-like object to save this asset to or the filename
|
||||
to use. If a filename is passed then a file is created with that
|
||||
filename and used instead.
|
||||
seek_begin: :class:`bool`
|
||||
Whether to seek to the beginning of the file after saving is
|
||||
successfully done.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
"""
|
||||
|
||||
data = await self.read()
|
||||
if isinstance(fp, io.BufferedIOBase):
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
else:
|
||||
with open(fp, 'wb') as f:
|
||||
return f.write(data)
|
||||
|
||||
async def to_file(
|
||||
self,
|
||||
*,
|
||||
filename: Optional[str] = MISSING,
|
||||
description: Optional[str] = None,
|
||||
spoiler: bool = False,
|
||||
) -> File:
|
||||
"""|coro|
|
||||
|
||||
Converts the asset into a :class:`File` suitable for sending via
|
||||
:meth:`abc.Messageable.send`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
filename: Optional[:class:`str`]
|
||||
The filename of the file. If not provided, then the filename from
|
||||
the asset's URL is used.
|
||||
description: Optional[:class:`str`]
|
||||
The description for the file.
|
||||
spoiler: :class:`bool`
|
||||
Whether the file is a spoiler.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
The asset does not have an associated state.
|
||||
ValueError
|
||||
The asset is a unicode emoji.
|
||||
TypeError
|
||||
The asset is a sticker with lottie type.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`File`
|
||||
The asset as a file suitable for sending.
|
||||
"""
|
||||
|
||||
data = await self.read()
|
||||
file_filename = filename if filename is not MISSING else yarl.URL(self.url).name
|
||||
return File(io.BytesIO(data), filename=file_filename, description=description, spoiler=spoiler)
|
||||
|
||||
|
||||
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 hash of the asset.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
'_state',
|
||||
'_url',
|
||||
'_animated',
|
||||
'_key',
|
||||
)
|
||||
|
||||
BASE = 'https://cdn.discordapp.com'
|
||||
|
||||
def __init__(self, state: _State, *, url: str, key: str, animated: bool = False) -> None:
|
||||
self._state: _State = state
|
||||
self._url: str = url
|
||||
self._animated: bool = animated
|
||||
self._key: str = key
|
||||
|
||||
@classmethod
|
||||
def _from_default_avatar(cls, state: _State, index: int) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/embed/avatars/{index}.png',
|
||||
key=str(index),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_avatar(cls, state: _State, user_id: int, avatar: str) -> Self:
|
||||
animated = avatar.startswith('a_')
|
||||
format = 'gif' if animated else 'png'
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024',
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_avatar(cls, state: _State, guild_id: int, member_id: int, avatar: str) -> Self:
|
||||
animated = avatar.startswith('a_')
|
||||
format = 'gif' if animated else 'png'
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024',
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_banner(cls, state: _State, guild_id: int, member_id: int, banner: str) -> Self:
|
||||
animated = banner.startswith('a_')
|
||||
format = 'gif' if animated else 'png'
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024',
|
||||
key=banner,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/avatar-decoration-presets/{avatar_decoration}.png?size=96',
|
||||
key=avatar_decoration,
|
||||
animated=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024',
|
||||
key=icon_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_app_icon(
|
||||
cls, state: _State, object_id: int, icon_hash: str, asset_type: Literal['icon', 'cover_image']
|
||||
) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/app-icons/{object_id}/{asset_type}.png?size=1024',
|
||||
key=icon_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_cover_image(cls, state: _State, object_id: int, cover_image_hash: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024',
|
||||
key=cover_image_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_scheduled_event_cover_image(cls, state: _State, scheduled_event_id: int, cover_image_hash: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/guild-events/{scheduled_event_id}/{cover_image_hash}.png?size=1024',
|
||||
key=cover_image_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_image(cls, state: _State, guild_id: int, image: str, path: str) -> Self:
|
||||
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: _State, guild_id: int, icon_hash: str) -> Self:
|
||||
animated = icon_hash.startswith('a_')
|
||||
format = 'gif' if animated else 'png'
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024',
|
||||
key=icon_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_sticker_banner(cls, state: _State, banner: int) -> Self:
|
||||
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: _State, user_id: int, banner_hash: str) -> Self:
|
||||
animated = banner_hash.startswith('a_')
|
||||
format = 'gif' if animated else 'png'
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512',
|
||||
key=banner_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_primary_guild(cls, state: _State, guild_id: int, icon_hash: str) -> Self:
|
||||
return cls(
|
||||
state,
|
||||
url=f'{cls.BASE}/guild-tag-badges/{guild_id}/{icon_hash}.png?size=64',
|
||||
key=icon_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._url
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._url)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
shorten = self._url.replace(self.BASE, '')
|
||||
return f'<Asset url={shorten!r}>'
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._url)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the underlying URL of the asset."""
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
""":class:`str`: Returns the identifying key of the asset."""
|
||||
return self._key
|
||||
|
||||
def is_animated(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is animated."""
|
||||
return self._animated
|
||||
|
||||
def replace(
|
||||
self,
|
||||
*,
|
||||
size: int = MISSING,
|
||||
format: ValidAssetFormatTypes = MISSING,
|
||||
static_format: ValidStaticFormatTypes = MISSING,
|
||||
) -> Self:
|
||||
"""Returns a new asset with the passed components replaced.
|
||||
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
``static_format`` is now preferred over ``format``
|
||||
if both are present and the asset is not animated.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
This function will now raise :exc:`ValueError` instead of
|
||||
``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
format: :class:`str`
|
||||
The new format to change it to. Must be either
|
||||
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
|
||||
static_format: :class:`str`
|
||||
The new format to change it to if the asset isn't animated.
|
||||
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
An invalid size or format was passed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
|
||||
if format is not MISSING:
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise ValueError(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||
else:
|
||||
if static_format is MISSING and format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f'format must be one of {VALID_STATIC_FORMATS}')
|
||||
url = url.with_path(f'{path}.{format}')
|
||||
|
||||
if static_format is not MISSING and not self._animated:
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f'static_format must be one of {VALID_STATIC_FORMATS}')
|
||||
url = url.with_path(f'{path}.{static_format}')
|
||||
|
||||
if size is not MISSING:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise ValueError('size must be a power of 2 between 16 and 4096')
|
||||
url = url.with_query(size=size)
|
||||
else:
|
||||
url = url.with_query(url.raw_query_string)
|
||||
|
||||
url = str(url)
|
||||
return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_size(self, size: int, /) -> Self:
|
||||
"""Returns a new asset with the specified size.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
This function will now raise :exc:`ValueError` instead of
|
||||
``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The asset had an invalid size.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
if not utils.valid_icon_size(size):
|
||||
raise ValueError('size must be a power of 2 between 16 and 4096')
|
||||
|
||||
url = str(yarl.URL(self._url).with_query(size=size))
|
||||
return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_format(self, format: ValidAssetFormatTypes, /) -> Self:
|
||||
"""Returns a new asset with the specified format.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
This function will now raise :exc:`ValueError` instead of
|
||||
``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
format: :class:`str`
|
||||
The new format of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise ValueError(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||
else:
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise ValueError(f'format must be one of {VALID_STATIC_FORMATS}')
|
||||
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string))
|
||||
return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_static_format(self, format: ValidStaticFormatTypes, /) -> Self:
|
||||
"""Returns a new asset with the specified static format.
|
||||
|
||||
This only changes the format if the underlying asset is
|
||||
not animated. Otherwise, the asset is not changed.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
This function will now raise :exc:`ValueError` instead of
|
||||
``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
format: :class:`str`
|
||||
The new static format of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
|
||||
if self._animated:
|
||||
return self
|
||||
return self.with_format(format)
|
||||
998
venv/lib/python3.12/site-packages/discord/audit_logs.py
Normal file
998
venv/lib/python3.12/site-packages/discord/audit_logs.py
Normal file
@@ -0,0 +1,998 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, Callable, ClassVar, Mapping, Generator, List, Optional, Tuple, Type, TypeVar, Union
|
||||
|
||||
from . import enums, flags, utils
|
||||
from .asset import Asset
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .automod import AutoModTrigger, AutoModRuleAction, AutoModRule
|
||||
from .role import Role
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .member import Member
|
||||
from .scheduled_event import ScheduledEvent
|
||||
from .stage_instance import StageInstance
|
||||
from .sticker import GuildSticker
|
||||
from .threads import Thread
|
||||
from .integrations import PartialIntegration
|
||||
from .channel import ForumChannel, StageChannel, ForumTag
|
||||
from .onboarding import OnboardingPrompt, OnboardingPromptOption
|
||||
|
||||
__all__ = (
|
||||
'AuditLogDiff',
|
||||
'AuditLogChanges',
|
||||
'AuditLogEntry',
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
||||
from . import abc
|
||||
from .guild import Guild
|
||||
from .state import ConnectionState
|
||||
from .types.audit_log import (
|
||||
AuditLogChange as AuditLogChangePayload,
|
||||
AuditLogEntry as AuditLogEntryPayload,
|
||||
_AuditLogChange_TriggerMetadata as AuditLogChangeTriggerMetadataPayload,
|
||||
)
|
||||
from .types.channel import (
|
||||
PermissionOverwrite as PermissionOverwritePayload,
|
||||
ForumTag as ForumTagPayload,
|
||||
DefaultReaction as DefaultReactionPayload,
|
||||
)
|
||||
from .types.invite import Invite as InvitePayload
|
||||
from .types.role import Role as RolePayload, RoleColours
|
||||
from .types.snowflake import Snowflake
|
||||
from .types.command import ApplicationCommandPermissions
|
||||
from .types.automod import AutoModerationAction
|
||||
from .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload
|
||||
from .user import User
|
||||
from .app_commands import AppCommand
|
||||
from .webhook import Webhook
|
||||
|
||||
TargetType = Union[
|
||||
Guild,
|
||||
abc.GuildChannel,
|
||||
Member,
|
||||
User,
|
||||
Role,
|
||||
Invite,
|
||||
Emoji,
|
||||
StageInstance,
|
||||
GuildSticker,
|
||||
Thread,
|
||||
Object,
|
||||
PartialIntegration,
|
||||
AutoModRule,
|
||||
ScheduledEvent,
|
||||
Webhook,
|
||||
AppCommand,
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
def _transform_timestamp(entry: AuditLogEntry, data: Optional[str]) -> Optional[datetime.datetime]:
|
||||
return utils.parse_time(data)
|
||||
|
||||
|
||||
def _transform_color(entry: AuditLogEntry, data: int) -> Colour:
|
||||
return Colour(data)
|
||||
|
||||
|
||||
def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int:
|
||||
return int(data)
|
||||
|
||||
|
||||
def _transform_channel(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Union[abc.GuildChannel, Object]]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
|
||||
def _transform_channels_or_threads(
|
||||
entry: AuditLogEntry, data: List[Snowflake]
|
||||
) -> List[Union[abc.GuildChannel, Thread, Object]]:
|
||||
return [entry.guild.get_channel_or_thread(int(data)) or Object(id=data) for data in data]
|
||||
|
||||
|
||||
def _transform_member_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
|
||||
def _transform_guild_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Guild]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._state._get_guild(int(data))
|
||||
|
||||
|
||||
def _transform_roles(entry: AuditLogEntry, data: List[Snowflake]) -> List[Union[Role, Object]]:
|
||||
return [entry.guild.get_role(int(role_id)) or Object(role_id, type=Role) for role_id in data]
|
||||
|
||||
|
||||
def _transform_applied_forum_tags(entry: AuditLogEntry, data: List[Snowflake]) -> List[Union[ForumTag, Object]]:
|
||||
thread = entry.target
|
||||
if isinstance(thread, Thread) and isinstance(thread.parent, ForumChannel):
|
||||
return [thread.parent.get_tag(tag_id) or Object(id=tag_id, type=ForumTag) for tag_id in map(int, data)]
|
||||
return [Object(id=tag_id, type=ForumTag) for tag_id in data]
|
||||
|
||||
|
||||
def _transform_overloaded_flags(entry: AuditLogEntry, data: int) -> Union[int, flags.ChannelFlags, flags.InviteFlags]:
|
||||
# The `flags` key is definitely overloaded. Right now it's for channels, threads and invites but
|
||||
# I am aware of `member.flags` and `user.flags` existing. However, this does not impact audit logs
|
||||
# at the moment but better safe than sorry.
|
||||
channel_audit_log_types = (
|
||||
enums.AuditLogAction.channel_create,
|
||||
enums.AuditLogAction.channel_update,
|
||||
enums.AuditLogAction.channel_delete,
|
||||
enums.AuditLogAction.thread_create,
|
||||
enums.AuditLogAction.thread_update,
|
||||
enums.AuditLogAction.thread_delete,
|
||||
)
|
||||
invite_audit_log_types = (
|
||||
enums.AuditLogAction.invite_create,
|
||||
enums.AuditLogAction.invite_update,
|
||||
enums.AuditLogAction.invite_delete,
|
||||
)
|
||||
|
||||
if entry.action in channel_audit_log_types:
|
||||
return flags.ChannelFlags._from_value(data)
|
||||
elif entry.action in invite_audit_log_types:
|
||||
return flags.InviteFlags._from_value(data)
|
||||
return data
|
||||
|
||||
|
||||
def _transform_forum_tags(entry: AuditLogEntry, data: List[ForumTagPayload]) -> List[ForumTag]:
|
||||
return [ForumTag.from_data(state=entry._state, data=d) for d in data]
|
||||
|
||||
|
||||
def _transform_default_reaction(entry: AuditLogEntry, data: DefaultReactionPayload) -> Optional[PartialEmoji]:
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
emoji_name = data.get('emoji_name') or ''
|
||||
emoji_id = utils._get_as_snowflake(data, 'emoji_id') or None # Coerce 0 -> None
|
||||
return PartialEmoji.with_state(state=entry._state, name=emoji_name, id=emoji_id)
|
||||
|
||||
|
||||
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, type=Role if ow_type == '0' else Member)
|
||||
|
||||
overwrites.append((target, ow))
|
||||
|
||||
return overwrites
|
||||
|
||||
|
||||
def _transform_icon(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
if entry.action is enums.AuditLogAction.guild_update:
|
||||
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
|
||||
else:
|
||||
return Asset._from_icon(entry._state, entry._target_id, data, path='role') # type: ignore # target_id won't be None in this case
|
||||
|
||||
|
||||
def _transform_avatar(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore # target_id won't be None in this case
|
||||
|
||||
|
||||
def _transform_cover_image(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_scheduled_event_cover_image(entry._state, entry._target_id, data) # type: ignore # target_id won't be None in this case
|
||||
|
||||
|
||||
def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]], Optional[Asset]]:
|
||||
def _transform(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAction]) -> List[AutoModRuleAction]:
|
||||
return [AutoModRuleAction.from_data(action) for action in data]
|
||||
|
||||
|
||||
def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji:
|
||||
return PartialEmoji(name=data)
|
||||
|
||||
|
||||
def _transform_onboarding_prompts(entry: AuditLogEntry, data: List[PromptPayload]) -> List[OnboardingPrompt]:
|
||||
return [OnboardingPrompt.from_dict(data=prompt, state=entry._state, guild=entry.guild) for prompt in data]
|
||||
|
||||
|
||||
def _transform_onboarding_prompt_options(
|
||||
entry: AuditLogEntry, data: List[PromptOptionPayload]
|
||||
) -> List[OnboardingPromptOption]:
|
||||
return [OnboardingPromptOption.from_dict(data=option, state=entry._state, guild=entry.guild) for option in data]
|
||||
|
||||
|
||||
E = TypeVar('E', bound=enums.Enum)
|
||||
|
||||
|
||||
def _enum_transformer(enum: Type[E]) -> Callable[[AuditLogEntry, int], E]:
|
||||
def _transform(entry: AuditLogEntry, data: int) -> E:
|
||||
return enums.try_enum(enum, data)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
F = TypeVar('F', bound=flags.BaseFlags)
|
||||
|
||||
|
||||
def _flag_transformer(cls: Type[F]) -> Callable[[AuditLogEntry, Union[int, str]], F]:
|
||||
def _transform(entry: AuditLogEntry, data: Union[int, str]) -> F:
|
||||
return cls._from_value(int(data))
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
def _transform_type(
|
||||
entry: AuditLogEntry, data: Union[int, str]
|
||||
) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str, enums.OnboardingPromptType]:
|
||||
if entry.action.name.startswith('sticker_'):
|
||||
return enums.try_enum(enums.StickerType, data)
|
||||
elif entry.action.name.startswith('integration_'):
|
||||
return data # type: ignore # integration type is str
|
||||
elif entry.action.name.startswith('webhook_'):
|
||||
return enums.try_enum(enums.WebhookType, data)
|
||||
elif entry.action.name.startswith('onboarding_prompt_'):
|
||||
return enums.try_enum(enums.OnboardingPromptType, data)
|
||||
else:
|
||||
return enums.try_enum(enums.ChannelType, data)
|
||||
|
||||
|
||||
class AuditLogDiff:
|
||||
def __len__(self) -> int:
|
||||
return len(self.__dict__)
|
||||
|
||||
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
||||
yield from self.__dict__.items()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
values = ' '.join('%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:
|
||||
# fmt: off
|
||||
TRANSFORMERS: ClassVar[Mapping[str, Tuple[Optional[str], Optional[Transformer]]]] = {
|
||||
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
|
||||
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
|
||||
'allow': (None, _flag_transformer(Permissions)),
|
||||
'deny': (None, _flag_transformer(Permissions)),
|
||||
'permissions': (None, _flag_transformer(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),
|
||||
'system_channel_flags': (None, _flag_transformer(flags.SystemChannelFlags)),
|
||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||
'rules_channel_id': ('rules_channel', _transform_channel),
|
||||
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
|
||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
|
||||
'banner_hash': ('banner', _guild_hash_transformer('banners')),
|
||||
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
|
||||
'icon_hash': ('icon', _transform_icon),
|
||||
'avatar_hash': ('avatar', _transform_avatar),
|
||||
'rate_limit_per_user': ('slowmode_delay', None),
|
||||
'default_thread_rate_limit_per_user': ('default_thread_slowmode_delay', None),
|
||||
'guild_id': ('guild', _transform_guild_id),
|
||||
'tags': ('emoji', None),
|
||||
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
|
||||
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
|
||||
'privacy_level': (None, _enum_transformer(enums.PrivacyLevel)),
|
||||
'format_type': (None, _enum_transformer(enums.StickerFormatType)),
|
||||
'type': (None, _transform_type),
|
||||
'communication_disabled_until': ('timed_out_until', _transform_timestamp),
|
||||
'expire_behavior': (None, _enum_transformer(enums.ExpireBehaviour)),
|
||||
'mfa_level': (None, _enum_transformer(enums.MFALevel)),
|
||||
'status': (None, _enum_transformer(enums.EventStatus)),
|
||||
'entity_type': (None, _enum_transformer(enums.EntityType)),
|
||||
'preferred_locale': (None, _enum_transformer(enums.Locale)),
|
||||
'image_hash': ('cover_image', _transform_cover_image),
|
||||
'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)),
|
||||
'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)),
|
||||
'actions': (None, _transform_automod_actions),
|
||||
'exempt_channels': (None, _transform_channels_or_threads),
|
||||
'exempt_roles': (None, _transform_roles),
|
||||
'applied_tags': (None, _transform_applied_forum_tags),
|
||||
'available_tags': (None, _transform_forum_tags),
|
||||
'flags': (None, _transform_overloaded_flags),
|
||||
'default_reaction_emoji': (None, _transform_default_reaction),
|
||||
'emoji_name': ('emoji', _transform_default_emoji),
|
||||
'user_id': ('user', _transform_member_id),
|
||||
'options': (None, _transform_onboarding_prompt_options),
|
||||
'prompts': (None, _transform_onboarding_prompts),
|
||||
'default_channel_ids': ('default_channels', _transform_channels_or_threads),
|
||||
'mode': (None, _enum_transformer(enums.OnboardingMode)),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]):
|
||||
self.before: AuditLogDiff = AuditLogDiff()
|
||||
self.after: AuditLogDiff = AuditLogDiff()
|
||||
# special case entire process since each
|
||||
# element in data is a different target
|
||||
# key is the target id
|
||||
if entry.action is enums.AuditLogAction.app_command_permission_update:
|
||||
self.before.app_command_permissions = []
|
||||
self.after.app_command_permissions = []
|
||||
|
||||
for elem in data:
|
||||
self._handle_app_command_permissions(
|
||||
self.before,
|
||||
entry,
|
||||
elem.get('old_value'), # type: ignore # value will be an ApplicationCommandPermissions if present
|
||||
)
|
||||
|
||||
self._handle_app_command_permissions(
|
||||
self.after,
|
||||
entry,
|
||||
elem.get('new_value'), # type: ignore # value will be an ApplicationCommandPermissions if present
|
||||
)
|
||||
return
|
||||
|
||||
for elem in data:
|
||||
attr = elem['key']
|
||||
|
||||
# special cases for role add/remove
|
||||
if attr == '$add':
|
||||
self._handle_role(self.before, self.after, entry, elem['new_value']) # type: ignore # new_value is a list of roles in this case
|
||||
continue
|
||||
elif attr == '$remove':
|
||||
self._handle_role(self.after, self.before, entry, elem['new_value']) # type: ignore # new_value is a list of roles in this case
|
||||
continue
|
||||
|
||||
# special case for automod trigger
|
||||
if attr == 'trigger_metadata':
|
||||
# given full metadata dict
|
||||
self._handle_trigger_metadata(entry, elem, data) # type: ignore # should be trigger metadata
|
||||
continue
|
||||
elif entry.action is enums.AuditLogAction.automod_rule_update and attr.startswith('$'):
|
||||
# on update, some trigger attributes are keys and formatted as $(add/remove)_{attribute}
|
||||
action, _, trigger_attr = attr.partition('_')
|
||||
# new_value should be a list of added/removed strings for keyword_filter, regex_patterns, or allow_list
|
||||
if action == '$add':
|
||||
self._handle_trigger_attr_update(self.before, self.after, entry, trigger_attr, elem['new_value']) # type: ignore
|
||||
elif action == '$remove':
|
||||
self._handle_trigger_attr_update(self.after, self.before, entry, trigger_attr, elem['new_value']) # type: ignore
|
||||
continue
|
||||
|
||||
# special case for colors to set secondary and tertiary colos/colour attributes
|
||||
if attr == 'colors':
|
||||
self._handle_colours(self.before, elem.get('old_value')) # type: ignore # should be a RoleColours dict
|
||||
self._handle_colours(self.after, elem.get('new_value')) # type: ignore # should be a RoleColours dict
|
||||
continue
|
||||
|
||||
try:
|
||||
key, transformer = self.TRANSFORMERS[attr]
|
||||
except (ValueError, KeyError):
|
||||
transformer = None
|
||||
else:
|
||||
if key:
|
||||
attr = key
|
||||
|
||||
transformer: Optional[Transformer]
|
||||
|
||||
try:
|
||||
before = elem['old_value']
|
||||
except KeyError:
|
||||
before = None
|
||||
else:
|
||||
if transformer:
|
||||
before = transformer(entry, before)
|
||||
|
||||
setattr(self.before, attr, before)
|
||||
|
||||
try:
|
||||
after = elem['new_value']
|
||||
except KeyError:
|
||||
after = None
|
||||
else:
|
||||
if transformer:
|
||||
after = transformer(entry, after)
|
||||
|
||||
setattr(self.after, attr, after)
|
||||
|
||||
# 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
|
||||
|
||||
for e in elem:
|
||||
role_id = int(e['id'])
|
||||
role = g.get_role(role_id)
|
||||
|
||||
if role is None:
|
||||
role = Object(id=role_id, type=Role)
|
||||
role.name = e['name'] # type: ignore # Object doesn't usually have name
|
||||
|
||||
data.append(role)
|
||||
|
||||
setattr(second, 'roles', data)
|
||||
|
||||
def _handle_app_command_permissions(
|
||||
self,
|
||||
diff: AuditLogDiff,
|
||||
entry: AuditLogEntry,
|
||||
data: Optional[ApplicationCommandPermissions],
|
||||
):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
# avoid circular import
|
||||
from discord.app_commands import AppCommandPermissions
|
||||
|
||||
state = entry._state
|
||||
guild = entry.guild
|
||||
diff.app_command_permissions.append(AppCommandPermissions(data=data, guild=guild, state=state))
|
||||
|
||||
def _handle_trigger_metadata(
|
||||
self,
|
||||
entry: AuditLogEntry,
|
||||
data: AuditLogChangeTriggerMetadataPayload,
|
||||
full_data: List[AuditLogChangePayload],
|
||||
):
|
||||
trigger_value: Optional[int] = None
|
||||
trigger_type: Optional[enums.AutoModRuleTriggerType] = None
|
||||
|
||||
# try to get trigger type from before or after
|
||||
trigger_type = getattr(self.before, 'trigger_type', getattr(self.after, 'trigger_type', None))
|
||||
|
||||
if trigger_type is None:
|
||||
if isinstance(entry.target, AutoModRule):
|
||||
# Trigger type cannot be changed, so it should be the same before and after updates.
|
||||
# Avoids checking which keys are in data to guess trigger type
|
||||
trigger_value = entry.target.trigger.type.value
|
||||
else:
|
||||
# found a trigger type from before or after
|
||||
trigger_value = trigger_type.value
|
||||
|
||||
if trigger_value is None:
|
||||
# try to find trigger type in the full list of changes
|
||||
_elem = utils.find(lambda elem: elem['key'] == 'trigger_type', full_data)
|
||||
if _elem is not None:
|
||||
trigger_value = _elem.get('old_value', _elem.get('new_value')) # type: ignore # trigger type values should be int
|
||||
|
||||
if trigger_value is None:
|
||||
# try to infer trigger_type from the keys in old or new value
|
||||
combined = (data.get('old_value') or {}).keys() | (data.get('new_value') or {}).keys()
|
||||
if not combined:
|
||||
trigger_value = enums.AutoModRuleTriggerType.spam.value
|
||||
elif 'presets' in combined:
|
||||
trigger_value = enums.AutoModRuleTriggerType.keyword_preset.value
|
||||
elif 'keyword_filter' in combined or 'regex_patterns' in combined:
|
||||
trigger_value = enums.AutoModRuleTriggerType.keyword.value
|
||||
elif 'mention_total_limit' in combined or 'mention_raid_protection_enabled' in combined:
|
||||
trigger_value = enums.AutoModRuleTriggerType.mention_spam.value
|
||||
else:
|
||||
# some unknown type
|
||||
trigger_value = -1
|
||||
|
||||
self.before.trigger = AutoModTrigger.from_data(trigger_value, data.get('old_value'))
|
||||
self.after.trigger = AutoModTrigger.from_data(trigger_value, data.get('new_value'))
|
||||
|
||||
def _handle_trigger_attr_update(
|
||||
self, first: AuditLogDiff, second: AuditLogDiff, entry: AuditLogEntry, attr: str, data: List[str]
|
||||
):
|
||||
self._create_trigger(first, entry)
|
||||
trigger = self._create_trigger(second, entry)
|
||||
try:
|
||||
# guard unexpecte non list attributes or non iterable data
|
||||
getattr(trigger, attr).extend(data)
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
def _handle_colours(self, diff: AuditLogDiff, colours: Optional[RoleColours]):
|
||||
if colours is not None:
|
||||
# handle colours to multiple colour attributes
|
||||
colour = Colour(colours['primary_color'])
|
||||
secondary_colour = colours['secondary_color']
|
||||
tertiary_colour = colours['tertiary_color']
|
||||
else:
|
||||
colour = None
|
||||
secondary_colour = None
|
||||
tertiary_colour = None
|
||||
|
||||
diff.color = diff.colour = colour
|
||||
diff.secondary_color = diff.secondary_colour = Colour(secondary_colour) if secondary_colour is not None else None
|
||||
diff.tertiary_color = diff.tertiary_colour = Colour(tertiary_colour) if tertiary_colour is not None else None
|
||||
|
||||
def _create_trigger(self, diff: AuditLogDiff, entry: AuditLogEntry) -> AutoModTrigger:
|
||||
# check if trigger has already been created
|
||||
if not hasattr(diff, 'trigger'):
|
||||
# create a trigger
|
||||
if isinstance(entry.target, AutoModRule):
|
||||
# get trigger type from the automod rule
|
||||
trigger_type = entry.target.trigger.type
|
||||
else:
|
||||
# unknown trigger type
|
||||
trigger_type = enums.try_enum(enums.AutoModRuleTriggerType, -1)
|
||||
|
||||
diff.trigger = AutoModTrigger(type=trigger_type)
|
||||
return diff.trigger
|
||||
|
||||
|
||||
class _AuditLogProxy:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class _AuditLogProxyMemberPrune(_AuditLogProxy):
|
||||
delete_member_days: int
|
||||
members_removed: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberMoveOrMessageDelete(_AuditLogProxy):
|
||||
channel: Union[abc.GuildChannel, Thread]
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberDisconnect(_AuditLogProxy):
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyPinAction(_AuditLogProxy):
|
||||
channel: Union[abc.GuildChannel, Thread]
|
||||
message_id: int
|
||||
|
||||
|
||||
class _AuditLogProxyStageInstanceAction(_AuditLogProxy):
|
||||
channel: abc.GuildChannel
|
||||
|
||||
|
||||
class _AuditLogProxyMessageBulkDelete(_AuditLogProxy):
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyAutoModAction(_AuditLogProxy):
|
||||
automod_rule_name: str
|
||||
automod_rule_trigger_type: str
|
||||
channel: Optional[Union[abc.GuildChannel, Thread]]
|
||||
|
||||
|
||||
class _AuditLogProxyAutoModActionQuarantineUser(_AuditLogProxy):
|
||||
automod_rule_name: str
|
||||
automod_rule_trigger_type: str
|
||||
|
||||
|
||||
class _AuditLogProxyMemberKickOrMemberRoleUpdate(_AuditLogProxy):
|
||||
integration_type: Optional[str]
|
||||
|
||||
|
||||
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`.
|
||||
user_id: Optional[:class:`int`]
|
||||
The user ID who initiated this action.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
id: :class:`int`
|
||||
The entry ID.
|
||||
guild: :class:`Guild`
|
||||
The guild that this entry belongs to.
|
||||
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: Mapping[int, User],
|
||||
integrations: Mapping[int, PartialIntegration],
|
||||
app_commands: Mapping[int, AppCommand],
|
||||
automod_rules: Mapping[int, AutoModRule],
|
||||
webhooks: Mapping[int, Webhook],
|
||||
data: AuditLogEntryPayload,
|
||||
guild: Guild,
|
||||
):
|
||||
self._state: ConnectionState = guild._state
|
||||
self.guild: Guild = guild
|
||||
self._users: Mapping[int, User] = users
|
||||
self._integrations: Mapping[int, PartialIntegration] = integrations
|
||||
self._app_commands: Mapping[int, AppCommand] = app_commands
|
||||
self._automod_rules: Mapping[int, AutoModRule] = automod_rules
|
||||
self._webhooks: Mapping[int, Webhook] = webhooks
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data: AuditLogEntryPayload) -> None:
|
||||
self.action: enums.AuditLogAction = enums.try_enum(enums.AuditLogAction, data['action_type'])
|
||||
self.id: int = int(data['id'])
|
||||
|
||||
# this key is technically not usually present
|
||||
self.reason: Optional[str] = data.get('reason')
|
||||
extra = data.get('options')
|
||||
|
||||
# fmt: off
|
||||
self.extra: Union[
|
||||
_AuditLogProxyMemberPrune,
|
||||
_AuditLogProxyMemberMoveOrMessageDelete,
|
||||
_AuditLogProxyMemberDisconnect,
|
||||
_AuditLogProxyPinAction,
|
||||
_AuditLogProxyStageInstanceAction,
|
||||
_AuditLogProxyMessageBulkDelete,
|
||||
_AuditLogProxyAutoModAction,
|
||||
_AuditLogProxyAutoModActionQuarantineUser,
|
||||
_AuditLogProxyMemberKickOrMemberRoleUpdate,
|
||||
Member, User, None, PartialIntegration,
|
||||
Role, Object
|
||||
] = None
|
||||
# fmt: on
|
||||
|
||||
if isinstance(self.action, enums.AuditLogAction) and extra:
|
||||
if self.action is enums.AuditLogAction.member_prune:
|
||||
# member prune has two keys with useful information
|
||||
self.extra = _AuditLogProxyMemberPrune(
|
||||
delete_member_days=int(extra['delete_member_days']),
|
||||
members_removed=int(extra['members_removed']),
|
||||
)
|
||||
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
|
||||
channel_id = int(extra['channel_id'])
|
||||
self.extra = _AuditLogProxyMemberMoveOrMessageDelete(
|
||||
count=int(extra['count']),
|
||||
channel=self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id),
|
||||
)
|
||||
elif self.action is enums.AuditLogAction.member_disconnect:
|
||||
# The member disconnect action has a dict with some information
|
||||
self.extra = _AuditLogProxyMemberDisconnect(count=int(extra['count']))
|
||||
elif self.action is enums.AuditLogAction.message_bulk_delete:
|
||||
# The bulk message delete action has the number of messages deleted
|
||||
self.extra = _AuditLogProxyMessageBulkDelete(count=int(extra['count']))
|
||||
elif self.action in (enums.AuditLogAction.kick, enums.AuditLogAction.member_role_update):
|
||||
# The member kick action has a dict with some information
|
||||
integration_type = extra.get('integration_type')
|
||||
self.extra = _AuditLogProxyMemberKickOrMemberRoleUpdate(integration_type=integration_type)
|
||||
elif self.action.name.endswith('pin'):
|
||||
# the pin actions have a dict with some information
|
||||
channel_id = int(extra['channel_id'])
|
||||
self.extra = _AuditLogProxyPinAction(
|
||||
channel=self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id),
|
||||
message_id=int(extra['message_id']),
|
||||
)
|
||||
elif (
|
||||
self.action is enums.AuditLogAction.automod_block_message
|
||||
or self.action is enums.AuditLogAction.automod_flag_message
|
||||
or self.action is enums.AuditLogAction.automod_timeout_member
|
||||
):
|
||||
channel_id = utils._get_as_snowflake(extra, 'channel_id')
|
||||
channel = None
|
||||
|
||||
# May be an empty string instead of None due to a Discord issue
|
||||
if channel_id:
|
||||
channel = self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id)
|
||||
|
||||
self.extra = _AuditLogProxyAutoModAction(
|
||||
automod_rule_name=extra['auto_moderation_rule_name'],
|
||||
automod_rule_trigger_type=enums.try_enum(
|
||||
enums.AutoModRuleTriggerType, int(extra['auto_moderation_rule_trigger_type'])
|
||||
),
|
||||
channel=channel,
|
||||
)
|
||||
elif self.action is enums.AuditLogAction.automod_quarantine_user:
|
||||
self.extra = _AuditLogProxyAutoModActionQuarantineUser(
|
||||
automod_rule_name=extra['auto_moderation_rule_name'],
|
||||
automod_rule_trigger_type=enums.try_enum(
|
||||
enums.AutoModRuleTriggerType, int(extra['auto_moderation_rule_trigger_type'])
|
||||
),
|
||||
)
|
||||
|
||||
elif self.action.name.startswith('overwrite_'):
|
||||
# the overwrite_ actions have a dict with some information
|
||||
instance_id = int(extra['id'])
|
||||
the_type = extra.get('type')
|
||||
if the_type == '1':
|
||||
self.extra = self._get_member(instance_id)
|
||||
elif the_type == '0':
|
||||
role = self.guild.get_role(instance_id)
|
||||
if role is None:
|
||||
role = Object(id=instance_id, type=Role)
|
||||
role.name = extra.get('role_name') # type: ignore # Object doesn't usually have name
|
||||
self.extra = role
|
||||
elif self.action.name.startswith('stage_instance'):
|
||||
channel_id = int(extra['channel_id'])
|
||||
self.extra = _AuditLogProxyStageInstanceAction(
|
||||
channel=self.guild.get_channel(channel_id) or Object(id=channel_id, type=StageChannel)
|
||||
)
|
||||
elif self.action.name.startswith('app_command'):
|
||||
app_id = int(extra['application_id'])
|
||||
self.extra = self._get_integration_by_app_id(app_id) or Object(app_id, type=PartialIntegration)
|
||||
|
||||
# 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_id: Optional[int] = utils._get_as_snowflake(data, 'user_id')
|
||||
self.user: Optional[Union[User, Member]] = self._get_member(self.user_id)
|
||||
self._target_id = utils._get_as_snowflake(data, 'target_id')
|
||||
|
||||
def _get_member(self, user_id: Optional[int]) -> Union[Member, User, None]:
|
||||
if user_id is None:
|
||||
return None
|
||||
|
||||
return self.guild.get_member(user_id) or self._users.get(user_id)
|
||||
|
||||
def _get_integration(self, integration_id: Optional[int]) -> Optional[PartialIntegration]:
|
||||
if integration_id is None:
|
||||
return None
|
||||
|
||||
return self._integrations.get(integration_id)
|
||||
|
||||
def _get_integration_by_app_id(self, application_id: Optional[int]) -> Optional[PartialIntegration]:
|
||||
if application_id is None:
|
||||
return None
|
||||
|
||||
# get PartialIntegration by application id
|
||||
return utils.get(self._integrations.values(), application_id=application_id)
|
||||
|
||||
def _get_app_command(self, app_command_id: Optional[int]) -> Optional[AppCommand]:
|
||||
if app_command_id is None:
|
||||
return None
|
||||
|
||||
return self._app_commands.get(app_command_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>'
|
||||
|
||||
@utils.cached_property
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@utils.cached_property
|
||||
def target(self) -> TargetType:
|
||||
if self.action.target_type is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
converter = getattr(self, '_convert_target_' + self.action.target_type)
|
||||
except AttributeError:
|
||||
if self._target_id is None:
|
||||
return None
|
||||
return Object(id=self._target_id)
|
||||
else:
|
||||
return converter(self._target_id)
|
||||
|
||||
@utils.cached_property
|
||||
def category(self) -> Optional[enums.AuditLogActionCategory]:
|
||||
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
||||
return self.action.category
|
||||
|
||||
@utils.cached_property
|
||||
def changes(self) -> AuditLogChanges:
|
||||
""":class:`AuditLogChanges`: The list of changes this entry has."""
|
||||
obj = AuditLogChanges(self, self._changes)
|
||||
del self._changes
|
||||
return obj
|
||||
|
||||
@utils.cached_property
|
||||
def before(self) -> AuditLogDiff:
|
||||
""":class:`AuditLogDiff`: The target's prior state."""
|
||||
return self.changes.before
|
||||
|
||||
@utils.cached_property
|
||||
def after(self) -> AuditLogDiff:
|
||||
""":class:`AuditLogDiff`: The target's subsequent state."""
|
||||
return self.changes.after
|
||||
|
||||
def _convert_target_guild(self, target_id: int) -> Guild:
|
||||
return self.guild
|
||||
|
||||
def _convert_target_channel(self, target_id: int) -> Union[abc.GuildChannel, Object]:
|
||||
return self.guild.get_channel(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_user(self, target_id: Optional[int]) -> Optional[Union[Member, User, Object]]:
|
||||
# For some reason the member_disconnect and member_move action types
|
||||
# do not have a non-null target_id so safeguard against that
|
||||
if target_id is None:
|
||||
return None
|
||||
|
||||
return self._get_member(target_id) or Object(id=target_id, type=Member)
|
||||
|
||||
def _convert_target_role(self, target_id: int) -> Union[Role, Object]:
|
||||
return self.guild.get_role(target_id) or Object(id=target_id, type=Role)
|
||||
|
||||
def _convert_target_invite(self, target_id: None) -> Invite:
|
||||
# invites have target_id set to null
|
||||
# so figure out which change has the full invite data
|
||||
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
|
||||
|
||||
fake_payload: InvitePayload = {
|
||||
'max_age': changeset.max_age,
|
||||
'max_uses': changeset.max_uses,
|
||||
'code': changeset.code,
|
||||
'temporary': changeset.temporary,
|
||||
'uses': changeset.uses,
|
||||
'channel': None, # type: ignore # the channel is passed to the Invite constructor directly
|
||||
}
|
||||
|
||||
obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel)
|
||||
try:
|
||||
obj.inviter = changeset.inviter
|
||||
except AttributeError:
|
||||
pass
|
||||
return obj
|
||||
|
||||
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
|
||||
return self._state.get_emoji(target_id) or Object(id=target_id, type=Emoji)
|
||||
|
||||
def _convert_target_message(self, target_id: Optional[int]) -> Optional[Union[Member, User, Object]]:
|
||||
# The message_pin and message_unpin action types do not have a
|
||||
# non-null target_id so safeguard against that
|
||||
|
||||
if target_id is None:
|
||||
return None
|
||||
|
||||
return self._get_member(target_id) or Object(id=target_id, type=Member)
|
||||
|
||||
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:
|
||||
return self.guild.get_stage_instance(target_id) or Object(id=target_id, type=StageInstance)
|
||||
|
||||
def _convert_target_sticker(self, target_id: int) -> Union[GuildSticker, Object]:
|
||||
return self._state.get_sticker(target_id) or Object(id=target_id, type=GuildSticker)
|
||||
|
||||
def _convert_target_thread(self, target_id: int) -> Union[Thread, Object]:
|
||||
return self.guild.get_thread(target_id) or Object(id=target_id, type=Thread)
|
||||
|
||||
def _convert_target_guild_scheduled_event(self, target_id: int) -> Union[ScheduledEvent, Object]:
|
||||
return self.guild.get_scheduled_event(target_id) or Object(id=target_id, type=ScheduledEvent)
|
||||
|
||||
def _convert_target_integration(self, target_id: int) -> Union[PartialIntegration, Object]:
|
||||
return self._get_integration(target_id) or Object(target_id, type=PartialIntegration)
|
||||
|
||||
def _convert_target_app_command(self, target_id: int) -> Union[AppCommand, Object]:
|
||||
target = self._get_app_command(target_id)
|
||||
if not target:
|
||||
# circular import
|
||||
from .app_commands import AppCommand
|
||||
|
||||
target = Object(target_id, type=AppCommand)
|
||||
|
||||
return target
|
||||
|
||||
def _convert_target_integration_or_app_command(self, target_id: int) -> Union[PartialIntegration, AppCommand, Object]:
|
||||
target = self._get_integration_by_app_id(target_id) or self._get_app_command(target_id)
|
||||
if not target:
|
||||
try:
|
||||
# circular import
|
||||
from .app_commands import AppCommand
|
||||
|
||||
# get application id from extras
|
||||
# if it matches target id, type should be integration
|
||||
target_app = self.extra
|
||||
# extra should be an Object or PartialIntegration
|
||||
app_id = target_app.application_id if isinstance(target_app, PartialIntegration) else target_app.id # type: ignore
|
||||
type = PartialIntegration if target_id == app_id else AppCommand
|
||||
except AttributeError:
|
||||
return Object(target_id)
|
||||
else:
|
||||
return Object(target_id, type=type)
|
||||
|
||||
return target
|
||||
|
||||
def _convert_target_auto_moderation(self, target_id: int) -> Union[AutoModRule, Object]:
|
||||
return self._automod_rules.get(target_id) or Object(target_id, type=AutoModRule)
|
||||
|
||||
def _convert_target_webhook(self, target_id: int) -> Union[Webhook, Object]:
|
||||
# circular import
|
||||
from .webhook import Webhook
|
||||
|
||||
return self._webhooks.get(target_id) or Object(target_id, type=Webhook)
|
||||
|
||||
def _convert_target_onboarding_prompt(self, target_id: int) -> Object:
|
||||
return Object(target_id, type=OnboardingPrompt)
|
||||
666
venv/lib/python3.12/site-packages/discord/automod.py
Normal file
666
venv/lib/python3.12/site-packages/discord/automod.py
Normal file
@@ -0,0 +1,666 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, Dict, Optional, List, Set, Union, Sequence, overload, Literal
|
||||
|
||||
from .enums import AutoModRuleTriggerType, AutoModRuleActionType, AutoModRuleEventType, try_enum
|
||||
from .flags import AutoModPresets
|
||||
from . import utils
|
||||
from .utils import MISSING, cached_slot_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from .abc import Snowflake, GuildChannel
|
||||
from .threads import Thread
|
||||
from .guild import Guild
|
||||
from .member import Member
|
||||
from .state import ConnectionState
|
||||
from .types.automod import (
|
||||
AutoModerationRule as AutoModerationRulePayload,
|
||||
AutoModerationTriggerMetadata as AutoModerationTriggerMetadataPayload,
|
||||
AutoModerationAction as AutoModerationActionPayload,
|
||||
AutoModerationActionExecution as AutoModerationActionExecutionPayload,
|
||||
)
|
||||
from .role import Role
|
||||
|
||||
__all__ = (
|
||||
'AutoModRuleAction',
|
||||
'AutoModTrigger',
|
||||
'AutoModRule',
|
||||
'AutoModAction',
|
||||
)
|
||||
|
||||
|
||||
class AutoModRuleAction:
|
||||
"""Represents an auto moderation's rule action.
|
||||
|
||||
.. note::
|
||||
Only one of ``channel_id``, ``duration``, or ``custom_message`` can be used.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
type: :class:`AutoModRuleActionType`
|
||||
The type of action to take.
|
||||
Defaults to :attr:`~AutoModRuleActionType.block_message`.
|
||||
channel_id: Optional[:class:`int`]
|
||||
The ID of the channel or thread to send the alert message to, if any.
|
||||
Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.send_alert_message`.
|
||||
duration: Optional[:class:`datetime.timedelta`]
|
||||
The duration of the timeout to apply, if any.
|
||||
Has a maximum of 28 days.
|
||||
Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.timeout`.
|
||||
custom_message: Optional[:class:`str`]
|
||||
A custom message which will be shown to a user when their message is blocked.
|
||||
Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.block_message`.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
"""
|
||||
|
||||
__slots__ = ('type', 'channel_id', 'duration', 'custom_message')
|
||||
|
||||
@overload
|
||||
def __init__(self, *, channel_id: int = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, *, type: Literal[AutoModRuleActionType.send_alert_message], channel_id: int = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, *, duration: datetime.timedelta = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, *, type: Literal[AutoModRuleActionType.timeout], duration: datetime.timedelta = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, *, custom_message: str = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message]) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, *, type: Literal[AutoModRuleActionType.block_message], custom_message: Optional[str] = ...
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: Optional[AutoModRuleActionType] = ...,
|
||||
channel_id: Optional[int] = ...,
|
||||
duration: Optional[datetime.timedelta] = ...,
|
||||
custom_message: Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: Optional[AutoModRuleActionType] = None,
|
||||
channel_id: Optional[int] = None,
|
||||
duration: Optional[datetime.timedelta] = None,
|
||||
custom_message: Optional[str] = None,
|
||||
) -> None:
|
||||
if sum(v is None for v in (channel_id, duration, custom_message)) < 2:
|
||||
raise ValueError('Only one of channel_id, duration, or custom_message can be passed.')
|
||||
|
||||
self.type: AutoModRuleActionType
|
||||
self.channel_id: Optional[int] = None
|
||||
self.duration: Optional[datetime.timedelta] = None
|
||||
self.custom_message: Optional[str] = None
|
||||
|
||||
if type is not None:
|
||||
self.type = type
|
||||
elif channel_id is not None:
|
||||
self.type = AutoModRuleActionType.send_alert_message
|
||||
elif duration is not None:
|
||||
self.type = AutoModRuleActionType.timeout
|
||||
else:
|
||||
self.type = AutoModRuleActionType.block_message
|
||||
|
||||
if self.type is AutoModRuleActionType.send_alert_message:
|
||||
if channel_id is None:
|
||||
raise ValueError('channel_id cannot be None if type is send_alert_message')
|
||||
self.channel_id = channel_id
|
||||
|
||||
if self.type is AutoModRuleActionType.timeout:
|
||||
if duration is None:
|
||||
raise ValueError('duration cannot be None set if type is timeout')
|
||||
self.duration = duration
|
||||
|
||||
if self.type is AutoModRuleActionType.block_message:
|
||||
self.custom_message = custom_message
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AutoModRuleAction type={self.type.value} channel={self.channel_id} duration={self.duration}>'
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: AutoModerationActionPayload) -> Self:
|
||||
if data['type'] == AutoModRuleActionType.timeout.value:
|
||||
duration_seconds = data['metadata']['duration_seconds']
|
||||
return cls(duration=datetime.timedelta(seconds=duration_seconds))
|
||||
elif data['type'] == AutoModRuleActionType.send_alert_message.value:
|
||||
channel_id = int(data['metadata']['channel_id'])
|
||||
return cls(channel_id=channel_id)
|
||||
elif data['type'] == AutoModRuleActionType.block_message.value:
|
||||
custom_message = data.get('metadata', {}).get('custom_message')
|
||||
return cls(type=AutoModRuleActionType.block_message, custom_message=custom_message)
|
||||
|
||||
return cls(type=AutoModRuleActionType.block_member_interactions)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
ret = {'type': self.type.value, 'metadata': {}}
|
||||
if self.type is AutoModRuleActionType.block_message and self.custom_message is not None:
|
||||
ret['metadata'] = {'custom_message': self.custom_message}
|
||||
elif self.type is AutoModRuleActionType.timeout:
|
||||
ret['metadata'] = {'duration_seconds': int(self.duration.total_seconds())} # type: ignore # duration cannot be None here
|
||||
elif self.type is AutoModRuleActionType.send_alert_message:
|
||||
ret['metadata'] = {'channel_id': str(self.channel_id)}
|
||||
return ret
|
||||
|
||||
|
||||
class AutoModTrigger:
|
||||
r"""Represents a trigger for an auto moderation rule.
|
||||
|
||||
The following table illustrates relevant attributes for each :class:`AutoModRuleTriggerType`:
|
||||
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
| Type | Attributes |
|
||||
+===============================================+================================================+
|
||||
| :attr:`AutoModRuleTriggerType.keyword` | :attr:`keyword_filter`, :attr:`regex_patterns`,|
|
||||
| | :attr:`allow_list` |
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
| :attr:`AutoModRuleTriggerType.spam` | |
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
| :attr:`AutoModRuleTriggerType.keyword_preset` | :attr:`presets`\, :attr:`allow_list` |
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
| :attr:`AutoModRuleTriggerType.mention_spam` | :attr:`mention_limit`, |
|
||||
| | :attr:`mention_raid_protection` |
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
| :attr:`AutoModRuleTriggerType.member_profile` | :attr:`keyword_filter`, :attr:`regex_patterns`,|
|
||||
| | :attr:`allow_list` |
|
||||
+-----------------------------------------------+------------------------------------------------+
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
type: :class:`AutoModRuleTriggerType`
|
||||
The type of trigger.
|
||||
keyword_filter: List[:class:`str`]
|
||||
The list of strings that will trigger the filter.
|
||||
Maximum of 1000. Keywords can only be up to 60 characters in length.
|
||||
|
||||
This could be combined with :attr:`regex_patterns`.
|
||||
regex_patterns: List[:class:`str`]
|
||||
The regex pattern that will trigger the filter. The syntax is based off of
|
||||
`Rust's regex syntax <https://docs.rs/regex/latest/regex/#syntax>`_.
|
||||
Maximum of 10. Regex strings can only be up to 260 characters in length.
|
||||
|
||||
This could be combined with :attr:`keyword_filter` and/or :attr:`allow_list`
|
||||
|
||||
.. versionadded:: 2.1
|
||||
presets: :class:`AutoModPresets`
|
||||
The presets used with the preset keyword filter.
|
||||
allow_list: List[:class:`str`]
|
||||
The list of words that are exempt from the commonly flagged words. Maximum of 100.
|
||||
Keywords can only be up to 60 characters in length.
|
||||
mention_limit: :class:`int`
|
||||
The total number of user and role mentions a message can contain.
|
||||
Has a maximum of 50.
|
||||
mention_raid_protection: :class:`bool`
|
||||
Whether mention raid protection is enabled or not.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'type',
|
||||
'keyword_filter',
|
||||
'presets',
|
||||
'allow_list',
|
||||
'mention_limit',
|
||||
'regex_patterns',
|
||||
'mention_raid_protection',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: Optional[AutoModRuleTriggerType] = None,
|
||||
keyword_filter: Optional[List[str]] = None,
|
||||
presets: Optional[AutoModPresets] = None,
|
||||
allow_list: Optional[List[str]] = None,
|
||||
mention_limit: Optional[int] = None,
|
||||
regex_patterns: Optional[List[str]] = None,
|
||||
mention_raid_protection: Optional[bool] = None,
|
||||
) -> None:
|
||||
unique_args = (keyword_filter or regex_patterns, presets, mention_limit or mention_raid_protection)
|
||||
if type is None and sum(arg is not None for arg in unique_args) > 1:
|
||||
raise ValueError(
|
||||
'Please pass only one of keyword_filter/regex_patterns, presets, or mention_limit/mention_raid_protection.'
|
||||
)
|
||||
|
||||
if type is not None:
|
||||
self.type = type
|
||||
elif keyword_filter is not None or regex_patterns is not None:
|
||||
self.type = AutoModRuleTriggerType.keyword
|
||||
elif presets is not None:
|
||||
self.type = AutoModRuleTriggerType.keyword_preset
|
||||
elif mention_limit is not None or mention_raid_protection is not None:
|
||||
self.type = AutoModRuleTriggerType.mention_spam
|
||||
else:
|
||||
raise ValueError(
|
||||
'Please pass the trigger type explicitly if not using keyword_filter, regex_patterns, presets, mention_limit, or mention_raid_protection.'
|
||||
)
|
||||
|
||||
self.keyword_filter: List[str] = keyword_filter if keyword_filter is not None else []
|
||||
self.presets: AutoModPresets = presets if presets is not None else AutoModPresets()
|
||||
self.allow_list: List[str] = allow_list if allow_list is not None else []
|
||||
self.mention_limit: int = mention_limit if mention_limit is not None else 0
|
||||
self.mention_raid_protection: bool = mention_raid_protection if mention_raid_protection is not None else False
|
||||
self.regex_patterns: List[str] = regex_patterns if regex_patterns is not None else []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
data = self.to_metadata_dict()
|
||||
if data:
|
||||
joined = ' '.join(f'{k}={v!r}' for k, v in data.items())
|
||||
return f'<AutoModTrigger type={self.type} {joined}>'
|
||||
|
||||
return f'<AutoModTrigger type={self.type}>'
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, type: int, data: Optional[AutoModerationTriggerMetadataPayload]) -> Self:
|
||||
type_ = try_enum(AutoModRuleTriggerType, type)
|
||||
if data is None:
|
||||
return cls(type=type_)
|
||||
elif type_ in (AutoModRuleTriggerType.keyword, AutoModRuleTriggerType.member_profile):
|
||||
return cls(
|
||||
type=type_,
|
||||
keyword_filter=data.get('keyword_filter'),
|
||||
regex_patterns=data.get('regex_patterns'),
|
||||
allow_list=data.get('allow_list'),
|
||||
)
|
||||
elif type_ is AutoModRuleTriggerType.keyword_preset:
|
||||
return cls(
|
||||
type=type_, presets=AutoModPresets._from_value(data.get('presets', [])), allow_list=data.get('allow_list')
|
||||
)
|
||||
elif type_ is AutoModRuleTriggerType.mention_spam:
|
||||
return cls(
|
||||
type=type_,
|
||||
mention_limit=data.get('mention_total_limit'),
|
||||
mention_raid_protection=data.get('mention_raid_protection_enabled'),
|
||||
)
|
||||
else:
|
||||
return cls(type=type_)
|
||||
|
||||
def to_metadata_dict(self) -> Optional[Dict[str, Any]]:
|
||||
if self.type in (AutoModRuleTriggerType.keyword, AutoModRuleTriggerType.member_profile):
|
||||
return {
|
||||
'keyword_filter': self.keyword_filter,
|
||||
'regex_patterns': self.regex_patterns,
|
||||
'allow_list': self.allow_list,
|
||||
}
|
||||
elif self.type is AutoModRuleTriggerType.keyword_preset:
|
||||
return {'presets': self.presets.to_array(), 'allow_list': self.allow_list}
|
||||
elif self.type is AutoModRuleTriggerType.mention_spam:
|
||||
return {
|
||||
'mention_total_limit': self.mention_limit,
|
||||
'mention_raid_protection_enabled': self.mention_raid_protection,
|
||||
}
|
||||
|
||||
|
||||
class AutoModRule:
|
||||
"""Represents an auto moderation rule.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The ID of the rule.
|
||||
guild: :class:`Guild`
|
||||
The guild the rule is for.
|
||||
name: :class:`str`
|
||||
The name of the rule.
|
||||
creator_id: :class:`int`
|
||||
The ID of the user that created the rule.
|
||||
trigger: :class:`AutoModTrigger`
|
||||
The rule's trigger.
|
||||
enabled: :class:`bool`
|
||||
Whether the rule is enabled.
|
||||
exempt_role_ids: Set[:class:`int`]
|
||||
The IDs of the roles that are exempt from the rule.
|
||||
exempt_channel_ids: Set[:class:`int`]
|
||||
The IDs of the channels that are exempt from the rule.
|
||||
event_type: :class:`AutoModRuleEventType`
|
||||
The type of event that will trigger the the rule.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_state',
|
||||
'_cs_exempt_roles',
|
||||
'_cs_exempt_channels',
|
||||
'_cs_actions',
|
||||
'id',
|
||||
'guild',
|
||||
'name',
|
||||
'creator_id',
|
||||
'event_type',
|
||||
'trigger',
|
||||
'enabled',
|
||||
'exempt_role_ids',
|
||||
'exempt_channel_ids',
|
||||
'_actions',
|
||||
)
|
||||
|
||||
def __init__(self, *, data: AutoModerationRulePayload, guild: Guild, state: ConnectionState) -> None:
|
||||
self._state: ConnectionState = state
|
||||
self.guild: Guild = guild
|
||||
self.id: int = int(data['id'])
|
||||
self.name: str = data['name']
|
||||
self.creator_id = int(data['creator_id'])
|
||||
self.event_type: AutoModRuleEventType = try_enum(AutoModRuleEventType, data['event_type'])
|
||||
self.trigger: AutoModTrigger = AutoModTrigger.from_data(data['trigger_type'], data=data.get('trigger_metadata'))
|
||||
self.enabled: bool = data['enabled']
|
||||
self.exempt_role_ids: Set[int] = {int(role_id) for role_id in data['exempt_roles']}
|
||||
self.exempt_channel_ids: Set[int] = {int(channel_id) for channel_id in data['exempt_channels']}
|
||||
self._actions: List[AutoModerationActionPayload] = data['actions']
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AutoModRule id={self.id} name={self.name!r} guild={self.guild!r}>'
|
||||
|
||||
def to_dict(self) -> AutoModerationRulePayload:
|
||||
ret: AutoModerationRulePayload = {
|
||||
'id': str(self.id),
|
||||
'guild_id': str(self.guild.id),
|
||||
'name': self.name,
|
||||
'creator_id': str(self.creator_id),
|
||||
'event_type': self.event_type.value,
|
||||
'trigger_type': self.trigger.type.value,
|
||||
'trigger_metadata': self.trigger.to_metadata_dict(),
|
||||
'actions': [action.to_dict() for action in self.actions],
|
||||
'enabled': self.enabled,
|
||||
'exempt_roles': [str(role_id) for role_id in self.exempt_role_ids],
|
||||
'exempt_channels': [str(channel_id) for channel_id in self.exempt_channel_ids],
|
||||
} # type: ignore # trigger types break the flow here.
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def creator(self) -> Optional[Member]:
|
||||
"""Optional[:class:`Member`]: The member that created this rule."""
|
||||
return self.guild.get_member(self.creator_id)
|
||||
|
||||
@cached_slot_property('_cs_exempt_roles')
|
||||
def exempt_roles(self) -> List[Role]:
|
||||
"""List[:class:`Role`]: The roles that are exempt from this rule."""
|
||||
result = []
|
||||
get_role = self.guild.get_role
|
||||
for role_id in self.exempt_role_ids:
|
||||
role = get_role(role_id)
|
||||
if role is not None:
|
||||
result.append(role)
|
||||
|
||||
return utils._unique(result)
|
||||
|
||||
@cached_slot_property('_cs_exempt_channels')
|
||||
def exempt_channels(self) -> List[Union[GuildChannel, Thread]]:
|
||||
"""List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The channels that are exempt from this rule."""
|
||||
it = filter(None, map(self.guild._resolve_channel, self.exempt_channel_ids))
|
||||
return utils._unique(it)
|
||||
|
||||
@cached_slot_property('_cs_actions')
|
||||
def actions(self) -> List[AutoModRuleAction]:
|
||||
"""List[:class:`AutoModRuleAction`]: The actions that are taken when this rule is triggered."""
|
||||
return [AutoModRuleAction.from_data(action) for action in self._actions]
|
||||
|
||||
def is_exempt(self, obj: Snowflake, /) -> bool:
|
||||
"""Check if an object is exempt from the automod rule.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
obj: :class:`abc.Snowflake`
|
||||
The role, channel, or thread to check.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`bool`
|
||||
Whether the object is exempt from the automod rule.
|
||||
"""
|
||||
return obj.id in self.exempt_channel_ids or obj.id in self.exempt_role_ids
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
event_type: AutoModRuleEventType = MISSING,
|
||||
actions: List[AutoModRuleAction] = MISSING,
|
||||
trigger: AutoModTrigger = MISSING,
|
||||
enabled: bool = MISSING,
|
||||
exempt_roles: Sequence[Snowflake] = MISSING,
|
||||
exempt_channels: Sequence[Snowflake] = MISSING,
|
||||
reason: str = MISSING,
|
||||
) -> Self:
|
||||
"""|coro|
|
||||
|
||||
Edits this auto moderation rule.
|
||||
|
||||
You must have :attr:`Permissions.manage_guild` to edit rules.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The new name to change to.
|
||||
event_type: :class:`AutoModRuleEventType`
|
||||
The new event type to change to.
|
||||
actions: List[:class:`AutoModRuleAction`]
|
||||
The new rule actions to update.
|
||||
trigger: :class:`AutoModTrigger`
|
||||
The new trigger to update.
|
||||
You can only change the trigger metadata, not the type.
|
||||
enabled: :class:`bool`
|
||||
Whether the rule should be enabled or not.
|
||||
exempt_roles: Sequence[:class:`abc.Snowflake`]
|
||||
The new roles to exempt from the rule.
|
||||
exempt_channels: Sequence[:class:`abc.Snowflake`]
|
||||
The new channels to exempt from the rule.
|
||||
reason: :class:`str`
|
||||
The reason for updating this rule. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to edit this rule.
|
||||
HTTPException
|
||||
Editing the rule failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`AutoModRule`
|
||||
The updated auto moderation rule.
|
||||
"""
|
||||
payload = {}
|
||||
if actions is not MISSING:
|
||||
payload['actions'] = [action.to_dict() for action in actions]
|
||||
|
||||
if name is not MISSING:
|
||||
payload['name'] = name
|
||||
|
||||
if event_type is not MISSING:
|
||||
payload['event_type'] = event_type.value
|
||||
|
||||
if trigger is not MISSING:
|
||||
trigger_metadata = trigger.to_metadata_dict()
|
||||
if trigger_metadata is not None:
|
||||
payload['trigger_metadata'] = trigger_metadata
|
||||
|
||||
if enabled is not MISSING:
|
||||
payload['enabled'] = enabled
|
||||
|
||||
if exempt_roles is not MISSING:
|
||||
payload['exempt_roles'] = [x.id for x in exempt_roles]
|
||||
|
||||
if exempt_channels is not MISSING:
|
||||
payload['exempt_channels'] = [x.id for x in exempt_channels]
|
||||
|
||||
data = await self._state.http.edit_auto_moderation_rule(
|
||||
self.guild.id,
|
||||
self.id,
|
||||
reason=reason,
|
||||
**payload,
|
||||
)
|
||||
|
||||
return self.__class__(data=data, guild=self.guild, state=self._state)
|
||||
|
||||
async def delete(self, *, reason: str = MISSING) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the auto moderation rule.
|
||||
|
||||
You must have :attr:`Permissions.manage_guild` to delete rules.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: :class:`str`
|
||||
The reason for deleting this rule. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to delete the rule.
|
||||
HTTPException
|
||||
Deleting the rule failed.
|
||||
"""
|
||||
await self._state.http.delete_auto_moderation_rule(self.guild.id, self.id, reason=reason)
|
||||
|
||||
|
||||
class AutoModAction:
|
||||
"""Represents an action that was taken as the result of a moderation rule.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
action: :class:`AutoModRuleAction`
|
||||
The action that was taken.
|
||||
message_id: Optional[:class:`int`]
|
||||
The message ID that triggered the action. This is only available if the
|
||||
action is done on an edited message.
|
||||
rule_id: :class:`int`
|
||||
The ID of the rule that was triggered.
|
||||
rule_trigger_type: :class:`AutoModRuleTriggerType`
|
||||
The trigger type of the rule that was triggered.
|
||||
guild_id: :class:`int`
|
||||
The ID of the guild where the rule was triggered.
|
||||
user_id: :class:`int`
|
||||
The ID of the user that triggered the rule.
|
||||
channel_id: :class:`int`
|
||||
The ID of the channel where the rule was triggered.
|
||||
alert_system_message_id: Optional[:class:`int`]
|
||||
The ID of the system message that was sent to the predefined alert channel.
|
||||
content: :class:`str`
|
||||
The content of the message that triggered the rule.
|
||||
Requires the :attr:`Intents.message_content` or it will always return an empty string.
|
||||
matched_keyword: Optional[:class:`str`]
|
||||
The matched keyword from the triggering message.
|
||||
matched_content: Optional[:class:`str`]
|
||||
The matched content from the triggering message.
|
||||
Requires the :attr:`Intents.message_content` or it will always return ``None``.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_state',
|
||||
'action',
|
||||
'rule_id',
|
||||
'rule_trigger_type',
|
||||
'guild_id',
|
||||
'user_id',
|
||||
'channel_id',
|
||||
'message_id',
|
||||
'alert_system_message_id',
|
||||
'content',
|
||||
'matched_keyword',
|
||||
'matched_content',
|
||||
)
|
||||
|
||||
def __init__(self, *, data: AutoModerationActionExecutionPayload, state: ConnectionState) -> None:
|
||||
self._state: ConnectionState = state
|
||||
self.message_id: Optional[int] = utils._get_as_snowflake(data, 'message_id')
|
||||
self.action: AutoModRuleAction = AutoModRuleAction.from_data(data['action'])
|
||||
self.rule_id: int = int(data['rule_id'])
|
||||
self.rule_trigger_type: AutoModRuleTriggerType = try_enum(AutoModRuleTriggerType, data['rule_trigger_type'])
|
||||
self.guild_id: int = int(data['guild_id'])
|
||||
self.channel_id: Optional[int] = utils._get_as_snowflake(data, 'channel_id')
|
||||
self.user_id: int = int(data['user_id'])
|
||||
self.alert_system_message_id: Optional[int] = utils._get_as_snowflake(data, 'alert_system_message_id')
|
||||
self.content: str = data.get('content', '')
|
||||
self.matched_keyword: Optional[str] = data['matched_keyword']
|
||||
self.matched_content: Optional[str] = data.get('matched_content')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AutoModRuleExecution rule_id={self.rule_id} action={self.action!r}>'
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild:
|
||||
""":class:`Guild`: The guild this action was taken in."""
|
||||
return self._state._get_or_create_unavailable_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def channel(self) -> Optional[Union[GuildChannel, Thread]]:
|
||||
"""Optional[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The channel this action was taken in."""
|
||||
if self.channel_id:
|
||||
return self.guild.get_channel_or_thread(self.channel_id)
|
||||
return None
|
||||
|
||||
@property
|
||||
def member(self) -> Optional[Member]:
|
||||
"""Optional[:class:`Member`]: The member this action was taken against /who triggered this rule."""
|
||||
return self.guild.get_member(self.user_id)
|
||||
|
||||
async def fetch_rule(self) -> AutoModRule:
|
||||
"""|coro|
|
||||
|
||||
Fetch the rule whose action was taken.
|
||||
|
||||
You must have :attr:`Permissions.manage_guild` to do this.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to view the rule.
|
||||
HTTPException
|
||||
Fetching the rule failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`AutoModRule`
|
||||
The rule that was executed.
|
||||
"""
|
||||
|
||||
data = await self._state.http.get_auto_moderation_rule(self.guild.id, self.rule_id)
|
||||
return AutoModRule(data=data, guild=self.guild, state=self._state)
|
||||
105
venv/lib/python3.12/site-packages/discord/backoff.py
Normal file
105
venv/lib/python3.12/site-packages/discord/backoff.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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 time
|
||||
import random
|
||||
from typing import Callable, Generic, Literal, TypeVar, overload, Union
|
||||
|
||||
T = TypeVar('T', bool, Literal[True], Literal[False])
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'ExponentialBackoff',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
|
||||
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[..., Union[int, float]] = rand.randrange if integral else rand.uniform
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[False]]) -> float: ...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[True]]) -> int: ...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]: ...
|
||||
|
||||
def delay(self) -> Union[int, float]:
|
||||
"""Compute the next delay
|
||||
|
||||
Returns the next delay to wait according to the exponential
|
||||
backoff algorithm. This is a value between 0 and base * 2^exp
|
||||
where exponent starts off at 1 and is incremented at every
|
||||
invocation of this method up to a maximum of 10.
|
||||
|
||||
If a period of more than base * 2^11 has passed since the last
|
||||
retry, the exponent is reset to 1.
|
||||
"""
|
||||
invocation = time.monotonic()
|
||||
interval = invocation - self._last_invocation
|
||||
self._last_invocation = invocation
|
||||
|
||||
if interval > self._reset_time:
|
||||
self._exp = 0
|
||||
|
||||
self._exp = min(self._exp + 1, self._max)
|
||||
return self._randfunc(0, self._base * 2**self._exp)
|
||||
28
venv/lib/python3.12/site-packages/discord/bin/COPYING
Normal file
28
venv/lib/python3.12/site-packages/discord/bin/COPYING
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 1994-2013 Xiph.Org Foundation and contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of the Xiph.Org Foundation nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
BIN
venv/lib/python3.12/site-packages/discord/bin/libopus-0.x64.dll
Normal file
BIN
venv/lib/python3.12/site-packages/discord/bin/libopus-0.x64.dll
Normal file
Binary file not shown.
BIN
venv/lib/python3.12/site-packages/discord/bin/libopus-0.x86.dll
Normal file
BIN
venv/lib/python3.12/site-packages/discord/bin/libopus-0.x86.dll
Normal file
Binary file not shown.
3674
venv/lib/python3.12/site-packages/discord/channel.py
Normal file
3674
venv/lib/python3.12/site-packages/discord/channel.py
Normal file
File diff suppressed because it is too large
Load Diff
3255
venv/lib/python3.12/site-packages/discord/client.py
Normal file
3255
venv/lib/python3.12/site-packages/discord/client.py
Normal file
File diff suppressed because it is too large
Load Diff
593
venv/lib/python3.12/site-packages/discord/colour.py
Normal file
593
venv/lib/python3.12/site-packages/discord/colour.py
Normal file
@@ -0,0 +1,593 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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
|
||||
import re
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = (
|
||||
'Colour',
|
||||
'Color',
|
||||
)
|
||||
|
||||
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9.]+%?)\s*,\s*(?P<g>[0-9.]+%?)\s*,\s*(?P<b>[0-9.]+%?)\s*\)')
|
||||
|
||||
|
||||
def parse_hex_number(argument: str) -> Colour:
|
||||
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
|
||||
try:
|
||||
value = int(arg, base=16)
|
||||
if not (0 <= value <= 0xFFFFFF):
|
||||
raise ValueError('hex number out of range for 24-bit colour')
|
||||
except ValueError:
|
||||
raise ValueError('invalid hex digit given') from None
|
||||
else:
|
||||
return Color(value=value)
|
||||
|
||||
|
||||
def parse_rgb_number(number: str) -> int:
|
||||
if number[-1] == '%':
|
||||
value = float(number[:-1])
|
||||
if not (0 <= value <= 100):
|
||||
raise ValueError('rgb percentage can only be between 0 to 100')
|
||||
return round(255 * (value / 100))
|
||||
|
||||
value = int(number)
|
||||
if not (0 <= value <= 255):
|
||||
raise ValueError('rgb number can only be between 0 to 255')
|
||||
return value
|
||||
|
||||
|
||||
def parse_rgb(argument: str, *, regex: re.Pattern[str] = RGB_REGEX) -> Colour:
|
||||
match = regex.match(argument)
|
||||
if match is None:
|
||||
raise ValueError('invalid rgb syntax found')
|
||||
|
||||
red = parse_rgb_number(match.group('r'))
|
||||
green = parse_rgb_number(match.group('g'))
|
||||
blue = parse_rgb_number(match.group('b'))
|
||||
return Color.from_rgb(red, green, blue)
|
||||
|
||||
|
||||
class Colour:
|
||||
"""Represents a Discord role colour. This class is similar
|
||||
to a (red, green, blue) :class:`tuple`.
|
||||
|
||||
There is an alias for this called Color.
|
||||
|
||||
.. 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.
|
||||
|
||||
.. note::
|
||||
|
||||
The colour values in the classmethods are mostly provided as-is and can change between
|
||||
versions should the Discord client's representation of that colour also change.
|
||||
|
||||
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
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, Colour) and self.value == other.value
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'#{self.value:0>6x}'
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Colour value={self.value}>'
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
@property
|
||||
def r(self) -> int:
|
||||
""":class:`int`: Returns the red component of the colour."""
|
||||
return self._get_byte(2)
|
||||
|
||||
@property
|
||||
def g(self) -> int:
|
||||
""":class:`int`: Returns the green component of the colour."""
|
||||
return self._get_byte(1)
|
||||
|
||||
@property
|
||||
def b(self) -> int:
|
||||
""":class:`int`: Returns the blue component of the colour."""
|
||||
return self._get_byte(0)
|
||||
|
||||
def to_rgb(self) -> Tuple[int, int, int]:
|
||||
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
|
||||
return (self.r, self.g, self.b)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls, r: int, g: int, b: int) -> Self:
|
||||
"""Constructs a :class:`Colour` from an RGB tuple."""
|
||||
return cls((r << 16) + (g << 8) + b)
|
||||
|
||||
@classmethod
|
||||
def from_hsv(cls, h: float, s: float, v: float) -> Self:
|
||||
"""Constructs a :class:`Colour` from an HSV tuple."""
|
||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, value: str) -> Colour:
|
||||
"""Constructs a :class:`Colour` from a string.
|
||||
|
||||
The following formats are accepted:
|
||||
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- ``rgb(<number>, <number>, <number>)``
|
||||
|
||||
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
|
||||
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #FFF).
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The string could not be converted into a colour.
|
||||
"""
|
||||
|
||||
if not value:
|
||||
raise ValueError('unknown colour format given')
|
||||
|
||||
if value[0] == '#':
|
||||
return parse_hex_number(value[1:])
|
||||
|
||||
if value[0:2] == '0x':
|
||||
rest = value[2:]
|
||||
# Legacy backwards compatible syntax
|
||||
if rest.startswith('#'):
|
||||
return parse_hex_number(rest[1:])
|
||||
return parse_hex_number(rest)
|
||||
|
||||
arg = value.lower()
|
||||
if arg[0:3] == 'rgb':
|
||||
return parse_rgb(arg)
|
||||
|
||||
raise ValueError('unknown colour format given')
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0``.
|
||||
|
||||
.. colour:: #000000
|
||||
"""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def random(cls, *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a random hue.
|
||||
|
||||
.. note::
|
||||
|
||||
The random algorithm works by choosing a colour with a random hue but
|
||||
with maxed out saturation and value.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
------------
|
||||
seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]]
|
||||
The seed to initialize the RNG with. If ``None`` is passed the default RNG is used.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
rand = random if seed is None else random.Random(seed)
|
||||
return cls.from_hsv(rand.random(), 1, 1)
|
||||
|
||||
@classmethod
|
||||
def teal(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1ABC9C``.
|
||||
|
||||
.. colour:: #1ABC9C
|
||||
"""
|
||||
return cls(0x1ABC9C)
|
||||
|
||||
@classmethod
|
||||
def dark_teal(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x11806A``.
|
||||
|
||||
.. colour:: #11806A
|
||||
"""
|
||||
return cls(0x11806A)
|
||||
|
||||
@classmethod
|
||||
def brand_green(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x57F287``.
|
||||
|
||||
.. colour:: #57F287
|
||||
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0x57F287)
|
||||
|
||||
@classmethod
|
||||
def green(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2ECC71``.
|
||||
|
||||
.. colour:: #2ECC71
|
||||
"""
|
||||
return cls(0x2ECC71)
|
||||
|
||||
@classmethod
|
||||
def dark_green(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1F8B4C``.
|
||||
|
||||
.. colour:: #1F8B4C
|
||||
"""
|
||||
return cls(0x1F8B4C)
|
||||
|
||||
@classmethod
|
||||
def blue(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x3498DB``.
|
||||
|
||||
.. colour:: #3498DB
|
||||
"""
|
||||
return cls(0x3498DB)
|
||||
|
||||
@classmethod
|
||||
def dark_blue(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``.
|
||||
|
||||
.. colour:: #206694
|
||||
"""
|
||||
return cls(0x206694)
|
||||
|
||||
@classmethod
|
||||
def purple(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x9B59B6``.
|
||||
|
||||
.. colour:: #9B59B6
|
||||
"""
|
||||
return cls(0x9B59B6)
|
||||
|
||||
@classmethod
|
||||
def dark_purple(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x71368A``.
|
||||
|
||||
.. colour:: #71368A
|
||||
"""
|
||||
return cls(0x71368A)
|
||||
|
||||
@classmethod
|
||||
def magenta(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xE91E63``.
|
||||
|
||||
.. colour:: #E91E63
|
||||
"""
|
||||
return cls(0xE91E63)
|
||||
|
||||
@classmethod
|
||||
def dark_magenta(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xAD1457``.
|
||||
|
||||
.. colour:: #AD1457
|
||||
"""
|
||||
return cls(0xAD1457)
|
||||
|
||||
@classmethod
|
||||
def gold(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xF1C40F``.
|
||||
|
||||
.. colour:: #F1C40F
|
||||
"""
|
||||
return cls(0xF1C40F)
|
||||
|
||||
@classmethod
|
||||
def dark_gold(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xC27C0E``.
|
||||
|
||||
.. colour:: #C27C0E
|
||||
"""
|
||||
return cls(0xC27C0E)
|
||||
|
||||
@classmethod
|
||||
def orange(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xE67E22``.
|
||||
|
||||
.. colour:: #E67E22
|
||||
"""
|
||||
return cls(0xE67E22)
|
||||
|
||||
@classmethod
|
||||
def dark_orange(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xA84300``.
|
||||
|
||||
.. colour:: #A84300
|
||||
"""
|
||||
return cls(0xA84300)
|
||||
|
||||
@classmethod
|
||||
def brand_red(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xED4245``.
|
||||
|
||||
.. colour:: #ED4245
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xED4245)
|
||||
|
||||
@classmethod
|
||||
def red(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xE74C3C``.
|
||||
|
||||
.. colour:: #E74C3C
|
||||
"""
|
||||
return cls(0xE74C3C)
|
||||
|
||||
@classmethod
|
||||
def dark_red(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x992D22``.
|
||||
|
||||
.. colour:: #992D22
|
||||
"""
|
||||
return cls(0x992D22)
|
||||
|
||||
@classmethod
|
||||
def lighter_grey(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x95A5A6``.
|
||||
|
||||
.. colour:: #95A5A6
|
||||
"""
|
||||
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``.
|
||||
|
||||
.. colour:: #607d8b
|
||||
"""
|
||||
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``.
|
||||
|
||||
.. colour:: #979C9F
|
||||
"""
|
||||
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``.
|
||||
|
||||
.. colour:: #546E7A
|
||||
"""
|
||||
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``.
|
||||
|
||||
.. colour:: #7289DA
|
||||
"""
|
||||
return cls(0x7289DA)
|
||||
|
||||
@classmethod
|
||||
def blurple(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x5865F2``.
|
||||
|
||||
.. colour:: #5865F2
|
||||
"""
|
||||
return cls(0x5865F2)
|
||||
|
||||
@classmethod
|
||||
def greyple(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x99AAB5``.
|
||||
|
||||
.. colour:: #99AAB5
|
||||
"""
|
||||
return cls(0x99AAB5)
|
||||
|
||||
@classmethod
|
||||
def ash_theme(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2E2E34``.
|
||||
|
||||
This will appear transparent on Discord's ash theme.
|
||||
|
||||
.. colour:: #2E2E34
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return cls(0x2E2E34)
|
||||
|
||||
@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.
|
||||
|
||||
.. colour:: #1A1A1E
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. versionchanged:: 2.2
|
||||
Updated colour from previous ``0x36393F`` to reflect discord theme changes.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Updated colour from previous ``0x313338`` to reflect discord theme changes.
|
||||
"""
|
||||
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.
|
||||
|
||||
.. colour:: #070709
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return cls(0x070709)
|
||||
|
||||
@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.
|
||||
|
||||
.. colour:: #FBFBFB
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return cls(0xFBFBFB)
|
||||
|
||||
@classmethod
|
||||
def fuchsia(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459E``.
|
||||
|
||||
.. colour:: #EB459E
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xEB459E)
|
||||
|
||||
@classmethod
|
||||
def yellow(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``.
|
||||
|
||||
.. colour:: #FEE75C
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xFEE75C)
|
||||
|
||||
@classmethod
|
||||
def ash_embed(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x37373E``.
|
||||
|
||||
.. colour:: #37373E
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
"""
|
||||
return cls(0x37373E)
|
||||
|
||||
@classmethod
|
||||
def dark_embed(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x242429``.
|
||||
|
||||
.. colour:: #242429
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Updated colour from previous ``0x2B2D31`` to reflect discord theme changes.
|
||||
"""
|
||||
return cls(0x242429)
|
||||
|
||||
@classmethod
|
||||
def onyx_embed(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x131416``.
|
||||
|
||||
.. colour:: #131416
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return cls(0x131416)
|
||||
|
||||
@classmethod
|
||||
def light_embed(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xFFFFFF``.
|
||||
|
||||
.. colour:: #EEEFF1
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
Updated colour from previous ``0xEEEFF1`` to reflect discord theme changes.
|
||||
"""
|
||||
return cls(0xFFFFFF)
|
||||
|
||||
@classmethod
|
||||
def pink(cls) -> Self:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459F``.
|
||||
|
||||
.. colour:: #EB459F
|
||||
|
||||
.. versionadded:: 2.3
|
||||
"""
|
||||
return cls(0xEB459F)
|
||||
|
||||
|
||||
Color = Colour
|
||||
1412
venv/lib/python3.12/site-packages/discord/components.py
Normal file
1412
venv/lib/python3.12/site-packages/discord/components.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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, Generator, Optional, Type, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .abc import Messageable, MessageableChannel
|
||||
|
||||
from types import TracebackType
|
||||
|
||||
BE = TypeVar('BE', bound=BaseException)
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'Typing',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
|
||||
def _typing_done_callback(fut: asyncio.Future) -> None:
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
fut.exception()
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Typing:
|
||||
def __init__(self, messageable: Messageable) -> None:
|
||||
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
|
||||
self.messageable: Messageable = messageable
|
||||
self.channel: Optional[MessageableChannel] = None
|
||||
|
||||
async def _get_channel(self) -> MessageableChannel:
|
||||
if self.channel:
|
||||
return self.channel
|
||||
|
||||
self.channel = channel = await self.messageable._get_channel()
|
||||
return channel
|
||||
|
||||
async def wrapped_typer(self) -> None:
|
||||
channel = await self._get_channel()
|
||||
await channel._state.http.send_typing(channel.id)
|
||||
|
||||
def __await__(self) -> Generator[None, None, None]:
|
||||
return self.wrapped_typer().__await__()
|
||||
|
||||
async def do_typing(self) -> None:
|
||||
channel = await self._get_channel()
|
||||
typing = channel._state.http.send_typing
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
await typing(channel.id)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
channel = await self._get_channel()
|
||||
await channel._state.http.send_typing(channel.id)
|
||||
self.task: asyncio.Task[None] = self.loop.create_task(self.do_typing())
|
||||
self.task.add_done_callback(_typing_done_callback)
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BE]],
|
||||
exc: Optional[BE],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
self.task.cancel()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user