Delete .venv directory
This commit is contained in:
committed by
GitHub
parent
7795984d81
commit
5a2693bd9f
@@ -1,67 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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__ = '1.7.3'
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from .client import Client
|
||||
from .appinfo import AppInfo
|
||||
from .user import User, ClientUser, Profile
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .activity import *
|
||||
from .channel import *
|
||||
from .guild import Guild
|
||||
from .flags import *
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .message import *
|
||||
from .asset import Asset
|
||||
from .errors import *
|
||||
from .calls import CallMessage, GroupCall
|
||||
from .permissions import Permissions, PermissionOverwrite
|
||||
from .role import Role, RoleTags
|
||||
from .file import File
|
||||
from .colour import Color, Colour
|
||||
from .integrations import Integration, IntegrationAccount
|
||||
from .invite import Invite, PartialInviteChannel, PartialInviteGuild
|
||||
from .template import Template
|
||||
from .widget import Widget, WidgetMember, WidgetChannel
|
||||
from .object import Object
|
||||
from .reaction import Reaction
|
||||
from . import utils, opus, abc
|
||||
from .enums import *
|
||||
from .embeds import Embed
|
||||
from .mentions import AllowedMentions
|
||||
from .shard import AutoShardedClient, ShardInfo
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
from .sticker import Sticker
|
||||
|
||||
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
|
||||
|
||||
version_info = VersionInfo(major=1, minor=7, micro=3, releaselevel='final', serial=0)
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
@@ -1,305 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import discord
|
||||
import pkg_resources
|
||||
import aiohttp
|
||||
import platform
|
||||
|
||||
def show_version():
|
||||
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':
|
||||
pkg = pkg_resources.get_distribution('discord.py')
|
||||
if pkg:
|
||||
entries.append(' - discord.py pkg_resources: v{0}'.format(pkg.version))
|
||||
|
||||
entries.append('- aiohttp v{0.__version__}'.format(aiohttp))
|
||||
uname = platform.uname()
|
||||
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
|
||||
print('\n'.join(entries))
|
||||
|
||||
def core(parser, args):
|
||||
if args.version:
|
||||
show_version()
|
||||
|
||||
bot_template = """#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import config
|
||||
|
||||
class Bot(commands.{base}):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs)
|
||||
for cog in config.cogs:
|
||||
try:
|
||||
self.load_extension(cog)
|
||||
except Exception as exc:
|
||||
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
|
||||
|
||||
async def on_ready(self):
|
||||
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
|
||||
|
||||
|
||||
bot = Bot()
|
||||
|
||||
# write general commands here
|
||||
|
||||
bot.run(config.token)
|
||||
"""
|
||||
|
||||
gitignore_template = """# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Our configuration files
|
||||
config.py
|
||||
"""
|
||||
|
||||
cog_template = '''# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
|
||||
class {name}(commands.Cog{attrs}):
|
||||
"""The description for {name} goes here."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
{extra}
|
||||
def setup(bot):
|
||||
bot.add_cog({name}(bot))
|
||||
'''
|
||||
|
||||
cog_extras = '''
|
||||
def cog_unload(self):
|
||||
# clean up logic goes here
|
||||
pass
|
||||
|
||||
async def cog_check(self, ctx):
|
||||
# checks that apply to every command in here
|
||||
return True
|
||||
|
||||
async def bot_check(self, ctx):
|
||||
# checks that apply to every command to the bot
|
||||
return True
|
||||
|
||||
async def bot_check_once(self, ctx):
|
||||
# check that apply to every command but is guaranteed to be called only once
|
||||
return True
|
||||
|
||||
async def cog_command_error(self, ctx, error):
|
||||
# error handling to every command in here
|
||||
pass
|
||||
|
||||
async def cog_before_invoke(self, ctx):
|
||||
# called before a command is called here
|
||||
pass
|
||||
|
||||
async def cog_after_invoke(self, ctx):
|
||||
# called after a command is called here
|
||||
pass
|
||||
|
||||
'''
|
||||
|
||||
|
||||
# certain file names and directory names are forbidden
|
||||
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
# although some of this doesn't apply to Linux, we might as well be consistent
|
||||
_base_table = {
|
||||
'<': '-',
|
||||
'>': '-',
|
||||
':': '-',
|
||||
'"': '-',
|
||||
# '/': '-', these are fine
|
||||
# '\\': '-',
|
||||
'|': '-',
|
||||
'?': '-',
|
||||
'*': '-',
|
||||
}
|
||||
|
||||
# NUL (0) and 1-31 are disallowed
|
||||
_base_table.update((chr(i), None) for i in range(32))
|
||||
|
||||
translation_table = str.maketrans(_base_table)
|
||||
|
||||
def to_path(parser, name, *, replace_spaces=False):
|
||||
if isinstance(name, Path):
|
||||
return name
|
||||
|
||||
if sys.platform == 'win32':
|
||||
forbidden = ('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', \
|
||||
'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9')
|
||||
if len(name) <= 4 and name.upper() in forbidden:
|
||||
parser.error('invalid directory name given, use a different one')
|
||||
|
||||
name = name.translate(translation_table)
|
||||
if replace_spaces:
|
||||
name = name.replace(' ', '-')
|
||||
return Path(name)
|
||||
|
||||
def newbot(parser, args):
|
||||
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('could not create our bot directory ({})'.format(exc))
|
||||
|
||||
cogs = new_directory / 'cogs'
|
||||
|
||||
try:
|
||||
cogs.mkdir(exist_ok=True)
|
||||
init = cogs / '__init__.py'
|
||||
init.touch()
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(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('could not create config file ({})'.format(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('could not create bot file ({})'.format(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('warning: could not create .gitignore file ({})'.format(exc))
|
||||
|
||||
print('successfully made bot at', new_directory)
|
||||
|
||||
def newcog(parser, args):
|
||||
cog_dir = to_path(parser, args.directory)
|
||||
try:
|
||||
cog_dir.mkdir(exist_ok=True)
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(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 += ', name="{}"'.format(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('could not create cog file ({})'.format(exc))
|
||||
else:
|
||||
print('successfully made cog at', directory)
|
||||
|
||||
def add_newbot_args(subparser):
|
||||
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):
|
||||
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():
|
||||
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():
|
||||
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.
File diff suppressed because it is too large
Load Diff
@@ -1,773 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from .asset import Asset
|
||||
from .enums import ActivityType, 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
|
||||
|
||||
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
|
||||
}
|
||||
"""
|
||||
|
||||
class BaseActivity:
|
||||
"""The base activity that all user-settable activities inherit from.
|
||||
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
|
||||
|
||||
The following types currently count as user-settable:
|
||||
|
||||
- :class:`Activity`
|
||||
- :class:`Game`
|
||||
- :class:`Streaming`
|
||||
- :class:`CustomActivity`
|
||||
|
||||
Note that although these types are considered user-settable by the library,
|
||||
Discord typically ignores certain combinations of activity depending on
|
||||
what is currently set. This behaviour may change in the future so there are
|
||||
no guarantees on whether Discord will actually let you set these types.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
__slots__ = ('_created_at',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._created_at = kwargs.pop('created_at', None)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""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.utcfromtimestamp(self._created_at / 1000)
|
||||
|
||||
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: :class:`int`
|
||||
The application ID of the game.
|
||||
name: :class:`str`
|
||||
The name of the activity.
|
||||
url: :class:`str`
|
||||
A stream URL that the activity could be doing.
|
||||
type: :class:`ActivityType`
|
||||
The type of activity currently being done.
|
||||
state: :class:`str`
|
||||
The user's current state. For example, "In Game".
|
||||
details: :class:`str`
|
||||
The detail of the user's current activity.
|
||||
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.
|
||||
- ``small_image``: A string representing the ID for the small image asset.
|
||||
- ``small_text``: A string representing the text when hovering over the small image asset.
|
||||
|
||||
party: :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).
|
||||
emoji: Optional[:class:`PartialEmoji`]
|
||||
The emoji that belongs to this activity.
|
||||
"""
|
||||
|
||||
__slots__ = ('state', 'details', '_created_at', 'timestamps', 'assets', 'party',
|
||||
'flags', 'sync_id', 'session_id', 'type', 'name', 'url',
|
||||
'application_id', 'emoji')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.state = kwargs.pop('state', None)
|
||||
self.details = kwargs.pop('details', None)
|
||||
self.timestamps = kwargs.pop('timestamps', {})
|
||||
self.assets = kwargs.pop('assets', {})
|
||||
self.party = kwargs.pop('party', {})
|
||||
self.application_id = _get_as_snowflake(kwargs, 'application_id')
|
||||
self.name = kwargs.pop('name', None)
|
||||
self.url = kwargs.pop('url', None)
|
||||
self.flags = kwargs.pop('flags', 0)
|
||||
self.sync_id = kwargs.pop('sync_id', None)
|
||||
self.session_id = kwargs.pop('session_id', None)
|
||||
self.type = try_enum(ActivityType, kwargs.pop('type', -1))
|
||||
emoji = kwargs.pop('emoji', None)
|
||||
if emoji is not None:
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
else:
|
||||
self.emoji = None
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
'type',
|
||||
'name',
|
||||
'url',
|
||||
'details',
|
||||
'application_id',
|
||||
'session_id',
|
||||
'emoji',
|
||||
)
|
||||
mapped = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in attrs)
|
||||
return '<Activity %s>' % mapped
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr, None)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, dict) and len(value) == 0:
|
||||
continue
|
||||
|
||||
ret[attr] = value
|
||||
ret['type'] = int(self.type)
|
||||
if self.emoji:
|
||||
ret['emoji'] = self.emoji.to_dict()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps['start'] / 1000)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps['end'] / 1000)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def large_image_url(self):
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
large_image = self.assets['large_image']
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, large_image)
|
||||
|
||||
@property
|
||||
def small_image_url(self):
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
small_image = self.assets['small_image']
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, small_image)
|
||||
@property
|
||||
def large_image_text(self):
|
||||
"""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[: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.
|
||||
start: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
|
||||
end: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The game's name.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', '_end', '_start')
|
||||
|
||||
def __init__(self, name, **extra):
|
||||
super().__init__(**extra)
|
||||
self.name = name
|
||||
|
||||
try:
|
||||
timestamps = extra['timestamps']
|
||||
except KeyError:
|
||||
self._extract_timestamp(extra, 'start')
|
||||
self._extract_timestamp(extra, 'end')
|
||||
else:
|
||||
self._start = timestamps.get('start', 0)
|
||||
self._end = timestamps.get('end', 0)
|
||||
|
||||
def _extract_timestamp(self, data, key):
|
||||
try:
|
||||
dt = data[key]
|
||||
except KeyError:
|
||||
setattr(self, '_' + key, 0)
|
||||
else:
|
||||
setattr(self, '_' + key, dt.timestamp() * 1000.0)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":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[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
|
||||
if self._start:
|
||||
return datetime.datetime.utcfromtimestamp(self._start / 1000)
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
|
||||
if self._end:
|
||||
return datetime.datetime.utcfromtimestamp(self._end / 1000)
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Game name={0.name!r}>'.format(self)
|
||||
|
||||
def to_dict(self):
|
||||
timestamps = {}
|
||||
if self._start:
|
||||
timestamps['start'] = self._start
|
||||
|
||||
if self._end:
|
||||
timestamps['end'] = self._end
|
||||
|
||||
return {
|
||||
'type': ActivityType.playing.value,
|
||||
'name': str(self.name),
|
||||
'timestamps': timestamps
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Game) and other.name == self.name
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
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: :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, url, **extra):
|
||||
super().__init__(**extra)
|
||||
self.platform = name
|
||||
self.name = extra.pop('details', name)
|
||||
self.game = extra.pop('state', None)
|
||||
self.url = url
|
||||
self.details = extra.pop('details', self.name) # compatibility
|
||||
self.assets = extra.pop('assets', {})
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":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):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Streaming name={0.name!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def twitch_name(self):
|
||||
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
|
||||
|
||||
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
|
||||
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
|
||||
"""
|
||||
|
||||
try:
|
||||
name = self.assets['large_image']
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return name[7:] if name[:7] == 'twitch:' else None
|
||||
|
||||
def to_dict(self):
|
||||
ret = {
|
||||
'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):
|
||||
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
class Spotify:
|
||||
"""Represents a Spotify listening activity from Discord. This is a special case of
|
||||
:class:`Activity` that makes it easier to work with the Spotify integration.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two activities are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two activities are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the activity's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the string 'Spotify'.
|
||||
"""
|
||||
|
||||
__slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id',
|
||||
'_created_at')
|
||||
|
||||
def __init__(self, **data):
|
||||
self._state = data.pop('state', None)
|
||||
self._details = data.pop('details', None)
|
||||
self._timestamps = data.pop('timestamps', {})
|
||||
self._assets = data.pop('assets', {})
|
||||
self._party = data.pop('party', {})
|
||||
self._sync_id = data.pop('sync_id')
|
||||
self._session_id = data.pop('session_id')
|
||||
self._created_at = data.pop('created_at', None)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":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[:class:`datetime.datetime`]: When the user started listening in UTC.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._created_at is not None:
|
||||
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
""":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):
|
||||
""":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):
|
||||
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):
|
||||
""":class:`str`: The activity's name. This will always return "Spotify"."""
|
||||
return 'Spotify'
|
||||
|
||||
def __eq__(self, other):
|
||||
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):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._session_id)
|
||||
|
||||
def __str__(self):
|
||||
return 'Spotify'
|
||||
|
||||
def __repr__(self):
|
||||
return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""":class:`str`: The title of the song being played."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
"""List[:class:`str`]: The artists of the song being played."""
|
||||
return self._state.split('; ')
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
""":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):
|
||||
""":class:`str`: The album that the song being played belongs to."""
|
||||
return self._assets.get('large_text', '')
|
||||
|
||||
@property
|
||||
def album_cover_url(self):
|
||||
""":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):
|
||||
""":class:`str`: The track ID used by Spotify to identify this song."""
|
||||
return self._sync_id
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps['start'] / 1000)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps['end'] / 1000)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
""":class:`datetime.timedelta`: The duration of the song being played."""
|
||||
return self.end - self.start
|
||||
|
||||
@property
|
||||
def party_id(self):
|
||||
""":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, *, emoji=None, **extra):
|
||||
super().__init__(**extra)
|
||||
self.name = name
|
||||
self.state = extra.pop('state', None)
|
||||
if self.name == 'Custom Status':
|
||||
self.name = self.state
|
||||
|
||||
if emoji is None:
|
||||
self.emoji = emoji
|
||||
elif isinstance(emoji, dict):
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
elif isinstance(emoji, str):
|
||||
self.emoji = PartialEmoji(name=emoji)
|
||||
elif isinstance(emoji, PartialEmoji):
|
||||
self.emoji = emoji
|
||||
else:
|
||||
raise TypeError('Expected str, PartialEmoji, or None, received {0!r} instead.'.format(type(emoji)))
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":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):
|
||||
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):
|
||||
return (isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, str(self.emoji)))
|
||||
|
||||
def __str__(self):
|
||||
if self.emoji:
|
||||
if self.name:
|
||||
return '%s %s' % (self.emoji, self.name)
|
||||
return str(self.emoji)
|
||||
else:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<CustomActivity name={0.name!r} emoji={0.emoji!r}>'.format(self)
|
||||
|
||||
|
||||
def create_activity(data):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
game_type = try_enum(ActivityType, data.get('type', -1))
|
||||
if game_type is ActivityType.playing:
|
||||
if 'application_id' in data or 'session_id' in data:
|
||||
return Activity(**data)
|
||||
return Game(**data)
|
||||
elif game_type is ActivityType.custom:
|
||||
try:
|
||||
name = data.pop('name')
|
||||
except KeyError:
|
||||
return Activity(**data)
|
||||
else:
|
||||
return CustomActivity(name=name, **data)
|
||||
elif game_type is ActivityType.streaming:
|
||||
if 'url' in data:
|
||||
return Streaming(**data)
|
||||
return Activity(**data)
|
||||
elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data:
|
||||
return Spotify(**data)
|
||||
return Activity(**data)
|
@@ -1,217 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 . import utils
|
||||
from .user import User
|
||||
from .asset import Asset
|
||||
from .team import Team
|
||||
|
||||
|
||||
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
|
||||
|
||||
icon: Optional[:class:`str`]
|
||||
The icon hash, if it exists.
|
||||
description: Optional[: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.
|
||||
summary: :class:`str`
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the summary field for the store page of its primary SKU.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
verify_key: :class:`str`
|
||||
The hex encoded key for verification in interactions and the
|
||||
GameSDK's `GetTicket <https://discord.com/developers/docs/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
|
||||
|
||||
cover_image: Optional[:class:`str`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the hash of the image on store embeds
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
__slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins',
|
||||
'bot_public', 'bot_require_code_grant', 'owner', 'icon',
|
||||
'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id',
|
||||
'slug', 'cover_image')
|
||||
|
||||
def __init__(self, state, data):
|
||||
self._state = state
|
||||
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.description = data['description']
|
||||
self.icon = data['icon']
|
||||
self.rpc_origins = data['rpc_origins']
|
||||
self.bot_public = data['bot_public']
|
||||
self.bot_require_code_grant = data['bot_require_code_grant']
|
||||
self.owner = User(state=self._state, data=data['owner'])
|
||||
|
||||
team = data.get('team')
|
||||
self.team = Team(state, team) if team else None
|
||||
|
||||
self.summary = data['summary']
|
||||
self.verify_key = data['verify_key']
|
||||
|
||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
|
||||
self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id')
|
||||
self.slug = data.get('slug')
|
||||
self.cover_image = data.get('cover_image')
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} description={0.description!r} public={0.bot_public} ' \
|
||||
'owner={0.owner!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`.Asset`: Retrieves the application's icon asset.
|
||||
|
||||
This is equivalent to calling :meth:`icon_url_as` with
|
||||
the default parameters ('webp' format and a size of 1024).
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
return self.icon_url_as()
|
||||
|
||||
def icon_url_as(self, *, format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the icon the application has.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: :class:`str`
|
||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_icon(self._state, self, 'app', format=format, size=size)
|
||||
|
||||
|
||||
@property
|
||||
def cover_image_url(self):
|
||||
""":class:`.Asset`: Retrieves the cover image on a store embed.
|
||||
|
||||
This is equivalent to calling :meth:`cover_image_url_as` with
|
||||
the default parameters ('webp' format and a size of 1024).
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
return self.cover_image_url_as()
|
||||
|
||||
def cover_image_url_as(self, *, format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the image on store embeds
|
||||
if this application is a game sold on Discord.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: :class:`str`
|
||||
The format to attempt to convert the image to. Defaults to 'webp'.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_cover_image(self._state, self, format=format, size=size)
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
"""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(int(self.guild_id))
|
@@ -1,262 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import io
|
||||
from .errors import DiscordException
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
class Asset:
|
||||
"""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:: bool(x)
|
||||
|
||||
Checks if the Asset has a 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__ = ('_state', '_url')
|
||||
|
||||
BASE = 'https://cdn.discordapp.com'
|
||||
|
||||
def __init__(self, state, url=None):
|
||||
self._state = state
|
||||
self._url = url
|
||||
|
||||
@classmethod
|
||||
def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
if format == "gif" and not user.is_avatar_animated():
|
||||
raise InvalidArgument("non animated avatars do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if user.avatar is None:
|
||||
return user.default_avatar_url
|
||||
|
||||
if format is None:
|
||||
format = 'gif' if user.is_avatar_animated() else static_format
|
||||
|
||||
return cls(state, '/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size))
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state, object, path, *, format='webp', size=1024):
|
||||
if object.icon is None:
|
||||
return cls(state)
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
|
||||
return cls(state, url)
|
||||
|
||||
@classmethod
|
||||
def _from_cover_image(cls, state, obj, *, format='webp', size=1024):
|
||||
if obj.cover_image is None:
|
||||
return cls(state)
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
|
||||
return cls(state, url)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if hash is None:
|
||||
return cls(state)
|
||||
|
||||
url = '/{key}/{0}/{1}.{2}?size={3}'
|
||||
return cls(state, url.format(id, hash, format, size, key=key))
|
||||
|
||||
@classmethod
|
||||
def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_AVATAR_FORMATS))
|
||||
if format == "gif" and not guild.is_icon_animated():
|
||||
raise InvalidArgument("non animated guild icons do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if guild.icon is None:
|
||||
return cls(state)
|
||||
|
||||
if format is None:
|
||||
format = 'gif' if guild.is_icon_animated() else static_format
|
||||
|
||||
return cls(state, '/icons/{0.id}/{0.icon}.{1}?size={2}'.format(guild, format, size))
|
||||
|
||||
@classmethod
|
||||
def _from_sticker_url(cls, state, sticker, *, size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
|
||||
return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size))
|
||||
|
||||
@classmethod
|
||||
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
if format == "gif" and not emoji.animated:
|
||||
raise InvalidArgument("non animated emoji's do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
if format is None:
|
||||
format = 'gif' if emoji.animated else static_format
|
||||
|
||||
return cls(state, '/emojis/{0.id}.{1}'.format(emoji, format))
|
||||
|
||||
def __str__(self):
|
||||
return self.BASE + self._url if self._url is not None else ''
|
||||
|
||||
def __len__(self):
|
||||
if self._url:
|
||||
return len(self.BASE + self._url)
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
return self._url is not None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Asset url={0._url!r}>'.format(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._url)
|
||||
|
||||
async def read(self):
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
|
||||
.. warning::
|
||||
|
||||
:class:`PartialEmoji` won't have a connection state if user created,
|
||||
and a URL won't be present if a custom image isn't associated with
|
||||
the asset, e.g. a guild with no custom icon.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no valid URL or internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if not self._url:
|
||||
raise DiscordException('Invalid asset (no URL provided)')
|
||||
|
||||
if self._state is None:
|
||||
raise DiscordException('Invalid state (no ConnectionState provided)')
|
||||
|
||||
return await self._state.http.get_from_cdn(self.BASE + self._url)
|
||||
|
||||
async def save(self, fp, *, seek_begin=True):
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fp: Union[BinaryIO, :class:`os.PathLike`]
|
||||
Same as in :meth:`Attachment.save`.
|
||||
seek_begin: :class:`bool`
|
||||
Same as in :meth:`Attachment.save`.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no valid URL or 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.IOBase) and fp.writable():
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
else:
|
||||
with open(fp, 'wb') as f:
|
||||
return f.write(data)
|
@@ -1,382 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 . import utils, enums
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
|
||||
def _transform_verification_level(entry, data):
|
||||
return enums.try_enum(enums.VerificationLevel, data)
|
||||
|
||||
def _transform_default_notifications(entry, data):
|
||||
return enums.try_enum(enums.NotificationLevel, data)
|
||||
|
||||
def _transform_explicit_content_filter(entry, data):
|
||||
return enums.try_enum(enums.ContentFilter, data)
|
||||
|
||||
def _transform_permissions(entry, data):
|
||||
return Permissions(data)
|
||||
|
||||
def _transform_color(entry, data):
|
||||
return Colour(data)
|
||||
|
||||
def _transform_snowflake(entry, data):
|
||||
return int(data)
|
||||
|
||||
def _transform_channel(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
def _transform_owner_id(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
def _transform_inviter_id(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
def _transform_overwrites(entry, data):
|
||||
overwrites = []
|
||||
for elem in data:
|
||||
allow = Permissions(elem['allow'])
|
||||
deny = Permissions(elem['deny'])
|
||||
ow = PermissionOverwrite.from_pair(allow, deny)
|
||||
|
||||
ow_type = elem['type']
|
||||
ow_id = int(elem['id'])
|
||||
if ow_type == 'role':
|
||||
target = entry.guild.get_role(ow_id)
|
||||
else:
|
||||
target = entry._get_member(ow_id)
|
||||
|
||||
if target is None:
|
||||
target = Object(id=ow_id)
|
||||
|
||||
overwrites.append((target, ow))
|
||||
|
||||
return overwrites
|
||||
|
||||
class AuditLogDiff:
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__dict__.items())
|
||||
|
||||
def __repr__(self):
|
||||
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
||||
return '<AuditLogDiff %s>' % values
|
||||
|
||||
class AuditLogChanges:
|
||||
TRANSFORMERS = {
|
||||
'verification_level': (None, _transform_verification_level),
|
||||
'explicit_content_filter': (None, _transform_explicit_content_filter),
|
||||
'allow': (None, _transform_permissions),
|
||||
'deny': (None, _transform_permissions),
|
||||
'permissions': (None, _transform_permissions),
|
||||
'id': (None, _transform_snowflake),
|
||||
'color': ('colour', _transform_color),
|
||||
'owner_id': ('owner', _transform_owner_id),
|
||||
'inviter_id': ('inviter', _transform_inviter_id),
|
||||
'channel_id': ('channel', _transform_channel),
|
||||
'afk_channel_id': ('afk_channel', _transform_channel),
|
||||
'system_channel_id': ('system_channel', _transform_channel),
|
||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||
'splash_hash': ('splash', None),
|
||||
'icon_hash': ('icon', None),
|
||||
'avatar_hash': ('avatar', None),
|
||||
'rate_limit_per_user': ('slowmode_delay', None),
|
||||
'default_message_notifications': ('default_notifications', _transform_default_notifications),
|
||||
}
|
||||
|
||||
def __init__(self, entry, data):
|
||||
self.before = AuditLogDiff()
|
||||
self.after = AuditLogDiff()
|
||||
|
||||
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'])
|
||||
continue
|
||||
elif attr == '$remove':
|
||||
self._handle_role(self.after, self.before, entry, elem['new_value'])
|
||||
continue
|
||||
|
||||
transformer = self.TRANSFORMERS.get(attr)
|
||||
if transformer:
|
||||
key, transformer = transformer
|
||||
if key:
|
||||
attr = key
|
||||
|
||||
try:
|
||||
before = elem['old_value']
|
||||
except KeyError:
|
||||
before = None
|
||||
else:
|
||||
if transformer:
|
||||
before = transformer(entry, before)
|
||||
|
||||
setattr(self.before, attr, before)
|
||||
|
||||
try:
|
||||
after = elem['new_value']
|
||||
except KeyError:
|
||||
after = None
|
||||
else:
|
||||
if transformer:
|
||||
after = transformer(entry, after)
|
||||
|
||||
setattr(self.after, attr, after)
|
||||
|
||||
# add an alias
|
||||
if hasattr(self.after, 'colour'):
|
||||
self.after.color = self.after.colour
|
||||
self.before.color = self.before.colour
|
||||
|
||||
def __repr__(self):
|
||||
return '<AuditLogChanges before=%r after=%r>' % (self.before, self.after)
|
||||
|
||||
def _handle_role(self, first, second, entry, elem):
|
||||
if not hasattr(first, 'roles'):
|
||||
setattr(first, 'roles', [])
|
||||
|
||||
data = []
|
||||
g = entry.guild
|
||||
|
||||
for e in elem:
|
||||
role_id = int(e['id'])
|
||||
role = g.get_role(role_id)
|
||||
|
||||
if role is None:
|
||||
role = Object(id=role_id)
|
||||
role.name = e['name']
|
||||
|
||||
data.append(role)
|
||||
|
||||
setattr(second, 'roles', data)
|
||||
|
||||
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: :class:`abc.User`
|
||||
The user who initiated this action. Usually a :class:`Member`\, unless gone
|
||||
then it's a :class:`User`.
|
||||
id: :class:`int`
|
||||
The entry ID.
|
||||
target: Any
|
||||
The target that got changed. The exact type of this depends on
|
||||
the action being done.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason this action was done.
|
||||
extra: Any
|
||||
Extra information that this entry has that might be useful.
|
||||
For most actions, this is ``None``. However in some cases it
|
||||
contains extra information. See :class:`AuditLogAction` for
|
||||
which actions have this field filled out.
|
||||
"""
|
||||
|
||||
def __init__(self, *, users, data, guild):
|
||||
self._state = guild._state
|
||||
self.guild = guild
|
||||
self._users = users
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
|
||||
self.id = int(data['id'])
|
||||
|
||||
# this key is technically not usually present
|
||||
self.reason = data.get('reason')
|
||||
self.extra = data.get('options')
|
||||
|
||||
if isinstance(self.action, enums.AuditLogAction) and self.extra:
|
||||
if self.action is enums.AuditLogAction.member_prune:
|
||||
# member prune has two keys with useful information
|
||||
self.extra = type('_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()})()
|
||||
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
|
||||
channel_id = int(self.extra['channel_id'])
|
||||
elems = {
|
||||
'count': int(self.extra['count']),
|
||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id)
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
elif self.action is enums.AuditLogAction.member_disconnect:
|
||||
# The member disconnect action has a dict with some information
|
||||
elems = {
|
||||
'count': int(self.extra['count']),
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
elif self.action.name.endswith('pin'):
|
||||
# the pin actions have a dict with some information
|
||||
channel_id = int(self.extra['channel_id'])
|
||||
message_id = int(self.extra['message_id'])
|
||||
elems = {
|
||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||
'message_id': message_id
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
elif self.action.name.startswith('overwrite_'):
|
||||
# the overwrite_ actions have a dict with some information
|
||||
instance_id = int(self.extra['id'])
|
||||
the_type = self.extra.get('type')
|
||||
if the_type == 'member':
|
||||
self.extra = self._get_member(instance_id)
|
||||
else:
|
||||
role = self.guild.get_role(instance_id)
|
||||
if role is None:
|
||||
role = Object(id=instance_id)
|
||||
role.name = self.extra.get('role_name')
|
||||
self.extra = role
|
||||
|
||||
# this key is not present when the above is present, typically.
|
||||
# It's a list of { new_value: a, old_value: b, key: c }
|
||||
# where new_value and old_value are not guaranteed to be there depending
|
||||
# on the action type, so let's just fetch it for now and only turn it
|
||||
# into meaningful data when requested
|
||||
self._changes = data.get('changes', [])
|
||||
|
||||
self.user = self._get_member(utils._get_as_snowflake(data, 'user_id'))
|
||||
self._target_id = utils._get_as_snowflake(data, 'target_id')
|
||||
|
||||
def _get_member(self, user_id):
|
||||
return self.guild.get_member(user_id) or self._users.get(user_id)
|
||||
|
||||
def __repr__(self):
|
||||
return '<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>'.format(self)
|
||||
|
||||
@utils.cached_property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@utils.cached_property
|
||||
def target(self):
|
||||
try:
|
||||
converter = getattr(self, '_convert_target_' + self.action.target_type)
|
||||
except AttributeError:
|
||||
return Object(id=self._target_id)
|
||||
else:
|
||||
return converter(self._target_id)
|
||||
|
||||
@utils.cached_property
|
||||
def category(self):
|
||||
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
||||
return self.action.category
|
||||
|
||||
@utils.cached_property
|
||||
def changes(self):
|
||||
""":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):
|
||||
""":class:`AuditLogDiff`: The target's prior state."""
|
||||
return self.changes.before
|
||||
|
||||
@utils.cached_property
|
||||
def after(self):
|
||||
""":class:`AuditLogDiff`: The target's subsequent state."""
|
||||
return self.changes.after
|
||||
|
||||
def _convert_target_guild(self, target_id):
|
||||
return self.guild
|
||||
|
||||
def _convert_target_channel(self, target_id):
|
||||
ch = self.guild.get_channel(target_id)
|
||||
if ch is None:
|
||||
return Object(id=target_id)
|
||||
return ch
|
||||
|
||||
def _convert_target_user(self, target_id):
|
||||
return self._get_member(target_id)
|
||||
|
||||
def _convert_target_role(self, target_id):
|
||||
role = self.guild.get_role(target_id)
|
||||
if role is None:
|
||||
return Object(id=target_id)
|
||||
return role
|
||||
|
||||
def _convert_target_invite(self, target_id):
|
||||
# invites have target_id set to null
|
||||
# so figure out which change has the full invite data
|
||||
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
|
||||
|
||||
fake_payload = {
|
||||
'max_age': changeset.max_age,
|
||||
'max_uses': changeset.max_uses,
|
||||
'code': changeset.code,
|
||||
'temporary': changeset.temporary,
|
||||
'channel': changeset.channel,
|
||||
'uses': changeset.uses,
|
||||
'guild': self.guild,
|
||||
}
|
||||
|
||||
obj = Invite(state=self._state, data=fake_payload)
|
||||
try:
|
||||
obj.inviter = changeset.inviter
|
||||
except AttributeError:
|
||||
pass
|
||||
return obj
|
||||
|
||||
def _convert_target_emoji(self, target_id):
|
||||
return self._state.get_emoji(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_message(self, target_id):
|
||||
return self._get_member(target_id)
|
@@ -1,85 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""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=1, *, integral=False):
|
||||
self._base = base
|
||||
|
||||
self._exp = 0
|
||||
self._max = 10
|
||||
self._reset_time = base * 2 ** 11
|
||||
self._last_invocation = time.monotonic()
|
||||
|
||||
# Use our own random instance to avoid messing with global one
|
||||
rand = random.Random()
|
||||
rand.seed()
|
||||
|
||||
self._randfunc = rand.randrange if integral else rand.uniform
|
||||
|
||||
def delay(self):
|
||||
"""Compute the next delay
|
||||
|
||||
Returns the next delay to wait according to the exponential
|
||||
backoff algorithm. This is a value between 0 and base * 2^exp
|
||||
where exponent starts off at 1 and is incremented at every
|
||||
invocation of this method up to a maximum of 10.
|
||||
|
||||
If a period of more than base * 2^11 has passed since the last
|
||||
retry, the exponent is reset to 1.
|
||||
"""
|
||||
invocation = time.monotonic()
|
||||
interval = invocation - self._last_invocation
|
||||
self._last_invocation = invocation
|
||||
|
||||
if interval > self._reset_time:
|
||||
self._exp = 0
|
||||
|
||||
self._exp = min(self._exp + 1, self._max)
|
||||
return self._randfunc(0, self._base * 2 ** self._exp)
|
Binary file not shown.
Binary file not shown.
@@ -1,176 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from . import utils
|
||||
from .enums import VoiceRegion, try_enum
|
||||
from .member import VoiceState
|
||||
|
||||
class CallMessage:
|
||||
"""Represents a group call message from Discord.
|
||||
|
||||
This is only received in cases where the message type is equivalent to
|
||||
:attr:`MessageType.call`.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
ended_timestamp: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC datetime object that represents the time that the call has ended.
|
||||
participants: List[:class:`User`]
|
||||
The list of users that are participating in this call.
|
||||
message: :class:`Message`
|
||||
The message associated with this call message.
|
||||
"""
|
||||
|
||||
def __init__(self, message, **kwargs):
|
||||
self.message = message
|
||||
self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp'))
|
||||
self.participants = kwargs.get('participants')
|
||||
|
||||
@property
|
||||
def call_ended(self):
|
||||
""":class:`bool`: Indicates if the call has ended.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
"""
|
||||
return self.ended_timestamp is not None
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: The private channel associated with this message.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Queries the duration of the call.
|
||||
|
||||
If the call has not ended then the current duration will
|
||||
be returned.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`datetime.timedelta`
|
||||
The timedelta object representing the duration.
|
||||
"""
|
||||
if self.ended_timestamp is None:
|
||||
return datetime.datetime.utcnow() - self.message.created_at
|
||||
else:
|
||||
return self.ended_timestamp - self.message.created_at
|
||||
|
||||
class GroupCall:
|
||||
"""Represents the actual group call from Discord.
|
||||
|
||||
This is accompanied with a :class:`CallMessage` denoting the information.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
call: :class:`CallMessage`
|
||||
The call message associated with this group call.
|
||||
unavailable: :class:`bool`
|
||||
Denotes if this group call is unavailable.
|
||||
ringing: List[:class:`User`]
|
||||
A list of users that are currently being rung to join the call.
|
||||
region: :class:`VoiceRegion`
|
||||
The guild region the group call is being hosted on.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.call = kwargs.get('call')
|
||||
self.unavailable = kwargs.get('unavailable')
|
||||
self._voice_states = {}
|
||||
|
||||
for state in kwargs.get('voice_states', []):
|
||||
self._update_voice_state(state)
|
||||
|
||||
self._update(**kwargs)
|
||||
|
||||
def _update(self, **kwargs):
|
||||
self.region = try_enum(VoiceRegion, kwargs.get('region'))
|
||||
lookup = {u.id: u for u in self.call.channel.recipients}
|
||||
me = self.call.channel.me
|
||||
lookup[me.id] = me
|
||||
self.ringing = list(filter(None, map(lookup.get, kwargs.get('ringing', []))))
|
||||
|
||||
def _update_voice_state(self, data):
|
||||
user_id = int(data['user_id'])
|
||||
# left the voice channel?
|
||||
if data['channel_id'] is None:
|
||||
self._voice_states.pop(user_id, None)
|
||||
else:
|
||||
self._voice_states[user_id] = VoiceState(data=data, channel=self.channel)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""List[:class:`User`]: A property that returns all users that are currently in this call.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
"""
|
||||
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
|
||||
me = self.channel.me
|
||||
if self.voice_state_for(me) is not None:
|
||||
ret.append(me)
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: Returns the channel the group call is in.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
"""
|
||||
return self.call.channel
|
||||
|
||||
@utils.deprecated()
|
||||
def voice_state_for(self, user):
|
||||
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
|
||||
|
||||
If the :class:`User` has no voice state then this function returns
|
||||
``None``.
|
||||
|
||||
.. deprecated:: 1.7
|
||||
|
||||
Parameters
|
||||
------------
|
||||
user: :class:`User`
|
||||
The user to retrieve the voice state for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`VoiceState`]
|
||||
The voice state associated with this user.
|
||||
"""
|
||||
|
||||
return self._voice_states.get(user.id)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,269 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import colorsys
|
||||
import random
|
||||
|
||||
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.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
value: :class:`int`
|
||||
The raw integer colour value.
|
||||
"""
|
||||
|
||||
__slots__ = ('value',)
|
||||
|
||||
def __init__(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int parameter, received %s instead.' % value.__class__.__name__)
|
||||
|
||||
self.value = value
|
||||
|
||||
def _get_byte(self, byte):
|
||||
return (self.value >> (8 * byte)) & 0xff
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Colour) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return '#{:0>6x}'.format(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Colour value=%s>' % self.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
@property
|
||||
def r(self):
|
||||
""":class:`int`: Returns the red component of the colour."""
|
||||
return self._get_byte(2)
|
||||
|
||||
@property
|
||||
def g(self):
|
||||
""":class:`int`: Returns the green component of the colour."""
|
||||
return self._get_byte(1)
|
||||
|
||||
@property
|
||||
def b(self):
|
||||
""":class:`int`: Returns the blue component of the colour."""
|
||||
return self._get_byte(0)
|
||||
|
||||
def to_rgb(self):
|
||||
"""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, g, b):
|
||||
"""Constructs a :class:`Colour` from an RGB tuple."""
|
||||
return cls((r << 16) + (g << 8) + b)
|
||||
|
||||
@classmethod
|
||||
def from_hsv(cls, h, s, v):
|
||||
"""Constructs a :class:`Colour` from an HSV tuple."""
|
||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def random(cls, *, seed=None):
|
||||
"""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):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
||||
return cls(0x1abc9c)
|
||||
|
||||
@classmethod
|
||||
def dark_teal(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
|
||||
return cls(0x11806a)
|
||||
|
||||
@classmethod
|
||||
def green(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
|
||||
return cls(0x2ecc71)
|
||||
|
||||
@classmethod
|
||||
def dark_green(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
|
||||
return cls(0x1f8b4c)
|
||||
|
||||
@classmethod
|
||||
def blue(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
|
||||
return cls(0x3498db)
|
||||
|
||||
@classmethod
|
||||
def dark_blue(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
|
||||
return cls(0x206694)
|
||||
|
||||
@classmethod
|
||||
def purple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
|
||||
return cls(0x9b59b6)
|
||||
|
||||
@classmethod
|
||||
def dark_purple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
|
||||
return cls(0x71368a)
|
||||
|
||||
@classmethod
|
||||
def magenta(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
|
||||
return cls(0xe91e63)
|
||||
|
||||
@classmethod
|
||||
def dark_magenta(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
|
||||
return cls(0xad1457)
|
||||
|
||||
@classmethod
|
||||
def gold(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
|
||||
return cls(0xf1c40f)
|
||||
|
||||
@classmethod
|
||||
def dark_gold(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
|
||||
return cls(0xc27c0e)
|
||||
|
||||
@classmethod
|
||||
def orange(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
|
||||
return cls(0xe67e22)
|
||||
|
||||
@classmethod
|
||||
def dark_orange(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
|
||||
return cls(0xa84300)
|
||||
|
||||
@classmethod
|
||||
def red(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
||||
return cls(0xe74c3c)
|
||||
|
||||
@classmethod
|
||||
def dark_red(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
|
||||
return cls(0x992d22)
|
||||
|
||||
@classmethod
|
||||
def lighter_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
|
||||
return cls(0x95a5a6)
|
||||
|
||||
lighter_gray = lighter_grey
|
||||
|
||||
@classmethod
|
||||
def dark_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
|
||||
return cls(0x607d8b)
|
||||
|
||||
dark_gray = dark_grey
|
||||
|
||||
@classmethod
|
||||
def light_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
|
||||
return cls(0x979c9f)
|
||||
|
||||
light_gray = light_grey
|
||||
|
||||
@classmethod
|
||||
def darker_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
|
||||
return cls(0x546e7a)
|
||||
|
||||
darker_gray = darker_grey
|
||||
|
||||
@classmethod
|
||||
def blurple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
|
||||
return cls(0x7289da)
|
||||
|
||||
@classmethod
|
||||
def greyple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
|
||||
return cls(0x99aab5)
|
||||
|
||||
@classmethod
|
||||
def dark_theme(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x36393F``.
|
||||
This will appear transparent on Discord's dark theme.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
return cls(0x36393F)
|
||||
|
||||
Color = Colour
|
@@ -1,67 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
def _typing_done_callback(fut):
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
fut.exception()
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
class Typing:
|
||||
def __init__(self, messageable):
|
||||
self.loop = messageable._state.loop
|
||||
self.messageable = messageable
|
||||
|
||||
async def do_typing(self):
|
||||
try:
|
||||
channel = self._channel
|
||||
except AttributeError:
|
||||
channel = await self.messageable._get_channel()
|
||||
|
||||
typing = channel._state.http.send_typing
|
||||
|
||||
while True:
|
||||
await typing(channel.id)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def __enter__(self):
|
||||
self.task = asyncio.ensure_future(self.do_typing(), loop=self.loop)
|
||||
self.task.add_done_callback(_typing_done_callback)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.task.cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
self._channel = channel = await self.messageable._get_channel()
|
||||
await channel._state.http.send_typing(channel.id)
|
||||
return self.__enter__()
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
self.task.cancel()
|
@@ -1,618 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from . import utils
|
||||
from .colour import Colour
|
||||
|
||||
class _EmptyEmbed:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return 'Embed.Empty'
|
||||
|
||||
def __len__(self):
|
||||
return 0
|
||||
|
||||
EmptyEmbed = _EmptyEmbed()
|
||||
|
||||
class EmbedProxy:
|
||||
def __init__(self, layer):
|
||||
self.__dict__.update(layer)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
return 'EmbedProxy(%s)' % ', '.join(('%s=%r' % (k, v) for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return EmptyEmbed
|
||||
|
||||
class Embed:
|
||||
"""Represents a Discord embed.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: len(x)
|
||||
|
||||
Returns the total size of the embed.
|
||||
Useful for checking if it's within the 6000 character limit.
|
||||
|
||||
Certain properties return an ``EmbedProxy``, a type
|
||||
that acts similar to a regular :class:`dict` except using dotted access,
|
||||
e.g. ``embed.author.icon_url``. If the attribute
|
||||
is invalid or empty, then a special sentinel value is returned,
|
||||
:attr:`Embed.Empty`.
|
||||
|
||||
For ease of use, all parameters that expect a :class:`str` are implicitly
|
||||
casted to :class:`str` for you.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
title: :class:`str`
|
||||
The title of the embed.
|
||||
This can be set during initialisation.
|
||||
type: :class:`str`
|
||||
The type of embed. Usually "rich".
|
||||
This can be set during initialisation.
|
||||
Possible strings for embed types can be found on discord's
|
||||
`api docs <https://discord.com/developers/docs/resources/channel#embed-object-embed-types>`_
|
||||
description: :class:`str`
|
||||
The description of the embed.
|
||||
This can be set during initialisation.
|
||||
url: :class:`str`
|
||||
The URL of the embed.
|
||||
This can be set during initialisation.
|
||||
timestamp: :class:`datetime.datetime`
|
||||
The timestamp of the embed content. This could be a naive or aware datetime.
|
||||
colour: Union[:class:`Colour`, :class:`int`]
|
||||
The colour code of the embed. Aliased to ``color`` as well.
|
||||
This can be set during initialisation.
|
||||
Empty
|
||||
A special sentinel value used by ``EmbedProxy`` and this class
|
||||
to denote that the value or attribute is empty.
|
||||
"""
|
||||
|
||||
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
|
||||
'_image', '_thumbnail', '_video', '_provider', '_author',
|
||||
'_fields', 'description')
|
||||
|
||||
Empty = EmptyEmbed
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# swap the colour/color aliases
|
||||
try:
|
||||
colour = kwargs['colour']
|
||||
except KeyError:
|
||||
colour = kwargs.get('color', EmptyEmbed)
|
||||
|
||||
self.colour = colour
|
||||
self.title = kwargs.get('title', EmptyEmbed)
|
||||
self.type = kwargs.get('type', 'rich')
|
||||
self.url = kwargs.get('url', EmptyEmbed)
|
||||
self.description = kwargs.get('description', EmptyEmbed)
|
||||
|
||||
if self.title is not EmptyEmbed:
|
||||
self.title = str(self.title)
|
||||
|
||||
if self.description is not EmptyEmbed:
|
||||
self.description = str(self.description)
|
||||
|
||||
if self.url is not EmptyEmbed:
|
||||
self.url = str(self.url)
|
||||
|
||||
try:
|
||||
timestamp = kwargs['timestamp']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
|
||||
format that Discord expects it to be in.
|
||||
|
||||
You can find out about this format in the `official Discord documentation`__.
|
||||
|
||||
.. _DiscordDocs: https://discord.com/developers/docs/resources/channel#embed-object
|
||||
|
||||
__ DiscordDocs_
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
data: :class:`dict`
|
||||
The dictionary to convert into an embed.
|
||||
"""
|
||||
# we are bypassing __init__ here since it doesn't apply here
|
||||
self = cls.__new__(cls)
|
||||
|
||||
# fill in the basic fields
|
||||
|
||||
self.title = data.get('title', EmptyEmbed)
|
||||
self.type = data.get('type', EmptyEmbed)
|
||||
self.description = data.get('description', EmptyEmbed)
|
||||
self.url = data.get('url', EmptyEmbed)
|
||||
|
||||
if self.title is not EmptyEmbed:
|
||||
self.title = str(self.title)
|
||||
|
||||
if self.description is not EmptyEmbed:
|
||||
self.description = str(self.description)
|
||||
|
||||
if self.url is not EmptyEmbed:
|
||||
self.url = str(self.url)
|
||||
|
||||
# try to fill in the more rich fields
|
||||
|
||||
try:
|
||||
self._colour = Colour(value=data['color'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._timestamp = utils.parse_time(data['timestamp'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for attr in ('thumbnail', 'video', 'provider', 'author', 'fields', 'image', 'footer'):
|
||||
try:
|
||||
value = data[attr]
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
setattr(self, '_' + attr, value)
|
||||
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
"""Returns a shallow copy of the embed."""
|
||||
return Embed.from_dict(self.to_dict())
|
||||
|
||||
def __len__(self):
|
||||
total = len(self.title) + len(self.description)
|
||||
for field in getattr(self, '_fields', []):
|
||||
total += len(field['name']) + len(field['value'])
|
||||
|
||||
try:
|
||||
footer = self._footer
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
total += len(footer['text'])
|
||||
|
||||
try:
|
||||
author = self._author
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
total += len(author['name'])
|
||||
|
||||
return total
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
return getattr(self, '_colour', EmptyEmbed)
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value):
|
||||
if isinstance(value, (Colour, _EmptyEmbed)):
|
||||
self._colour = value
|
||||
elif isinstance(value, int):
|
||||
self._colour = Colour(value=value)
|
||||
else:
|
||||
raise TypeError('Expected discord.Colour, int, or Embed.Empty but received %s instead.' % value.__class__.__name__)
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return getattr(self, '_timestamp', EmptyEmbed)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, value):
|
||||
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
|
||||
self._timestamp = value
|
||||
else:
|
||||
raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__)
|
||||
|
||||
@property
|
||||
def footer(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the footer contents.
|
||||
|
||||
See :meth:`set_footer` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_footer', {}))
|
||||
|
||||
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
"""Sets the footer for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
text: :class:`str`
|
||||
The footer text.
|
||||
icon_url: :class:`str`
|
||||
The URL of the footer icon. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._footer = {}
|
||||
if text is not EmptyEmbed:
|
||||
self._footer['text'] = str(text)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._footer['icon_url'] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the image contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
- ``url``
|
||||
- ``proxy_url``
|
||||
- ``width``
|
||||
- ``height``
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_image', {}))
|
||||
|
||||
def set_image(self, *, url):
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
.. versionchanged:: 1.4
|
||||
Passing :attr:`Empty` removes the image.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
url: :class:`str`
|
||||
The source URL for the image. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
if url is EmptyEmbed:
|
||||
try:
|
||||
del self._image
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._image = {
|
||||
'url': str(url)
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
- ``url``
|
||||
- ``proxy_url``
|
||||
- ``width``
|
||||
- ``height``
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {}))
|
||||
|
||||
def set_thumbnail(self, *, url):
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
.. versionchanged:: 1.4
|
||||
Passing :attr:`Empty` removes the thumbnail.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
url: :class:`str`
|
||||
The source URL for the thumbnail. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
if url is EmptyEmbed:
|
||||
try:
|
||||
del self._thumbnail
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._thumbnail = {
|
||||
'url': str(url)
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the video contents.
|
||||
|
||||
Possible attributes include:
|
||||
|
||||
- ``url`` for the video URL.
|
||||
- ``height`` for the video height.
|
||||
- ``width`` for the video width.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_video', {}))
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the provider contents.
|
||||
|
||||
The only attributes that might be accessed are ``name`` and ``url``.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_provider', {}))
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the author contents.
|
||||
|
||||
See :meth:`set_author` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_author', {}))
|
||||
|
||||
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
"""Sets the author for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the author.
|
||||
url: :class:`str`
|
||||
The URL for the author.
|
||||
icon_url: :class:`str`
|
||||
The URL of the author icon. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._author = {
|
||||
'name': str(name)
|
||||
}
|
||||
|
||||
if url is not EmptyEmbed:
|
||||
self._author['url'] = str(url)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._author['icon_url'] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
def remove_author(self):
|
||||
"""Clears embed's author information.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
try:
|
||||
del self._author
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
||||
|
||||
See :meth:`add_field` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])]
|
||||
|
||||
def add_field(self, *, name, value, inline=True):
|
||||
"""Adds a field to the embed object.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the field.
|
||||
value: :class:`str`
|
||||
The value of the field.
|
||||
inline: :class:`bool`
|
||||
Whether the field should be displayed inline.
|
||||
"""
|
||||
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
}
|
||||
|
||||
try:
|
||||
self._fields.append(field)
|
||||
except AttributeError:
|
||||
self._fields = [field]
|
||||
|
||||
return self
|
||||
|
||||
def insert_field_at(self, index, *, name, value, inline=True):
|
||||
"""Inserts a field before a specified index to the embed.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
index: :class:`int`
|
||||
The index of where to insert the field.
|
||||
name: :class:`str`
|
||||
The name of the field.
|
||||
value: :class:`str`
|
||||
The value of the field.
|
||||
inline: :class:`bool`
|
||||
Whether the field should be displayed inline.
|
||||
"""
|
||||
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
}
|
||||
|
||||
try:
|
||||
self._fields.insert(index, field)
|
||||
except AttributeError:
|
||||
self._fields = [field]
|
||||
|
||||
return self
|
||||
|
||||
def clear_fields(self):
|
||||
"""Removes all fields from this embed."""
|
||||
try:
|
||||
self._fields.clear()
|
||||
except AttributeError:
|
||||
self._fields = []
|
||||
|
||||
def remove_field(self, index):
|
||||
"""Removes a field at a specified index.
|
||||
|
||||
If the index is invalid or out of bounds then the error is
|
||||
silently swallowed.
|
||||
|
||||
.. note::
|
||||
|
||||
When deleting a field by index, the index of the other fields
|
||||
shift to fill the gap just like a regular list.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
index: :class:`int`
|
||||
The index of the field to remove.
|
||||
"""
|
||||
try:
|
||||
del self._fields[index]
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
def set_field_at(self, index, *, name, value, inline=True):
|
||||
"""Modifies a field to the embed object.
|
||||
|
||||
The index must point to a valid pre-existing field.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
index: :class:`int`
|
||||
The index of the field to modify.
|
||||
name: :class:`str`
|
||||
The name of the field.
|
||||
value: :class:`str`
|
||||
The value of the field.
|
||||
inline: :class:`bool`
|
||||
Whether the field should be displayed inline.
|
||||
|
||||
Raises
|
||||
-------
|
||||
IndexError
|
||||
An invalid index was provided.
|
||||
"""
|
||||
|
||||
try:
|
||||
field = self._fields[index]
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
raise IndexError('field index out of range')
|
||||
|
||||
field['name'] = str(name)
|
||||
field['value'] = str(value)
|
||||
field['inline'] = inline
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
"""Converts this embed object into a dict."""
|
||||
|
||||
# add in the raw data into the dict
|
||||
result = {
|
||||
key[1:]: getattr(self, key)
|
||||
for key in self.__slots__
|
||||
if key[0] == '_' and hasattr(self, key)
|
||||
}
|
||||
|
||||
# deal with basic convenience wrappers
|
||||
|
||||
try:
|
||||
colour = result.pop('colour')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if colour:
|
||||
result['color'] = colour.value
|
||||
|
||||
try:
|
||||
timestamp = result.pop('timestamp')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if timestamp:
|
||||
if timestamp.tzinfo:
|
||||
result['timestamp'] = timestamp.astimezone(tz=datetime.timezone.utc).isoformat()
|
||||
else:
|
||||
result['timestamp'] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
# add in the non raw attribute ones
|
||||
if self.type:
|
||||
result['type'] = self.type
|
||||
|
||||
if self.description:
|
||||
result['description'] = self.description
|
||||
|
||||
if self.url:
|
||||
result['url'] = self.url
|
||||
|
||||
if self.title:
|
||||
result['title'] = self.title
|
||||
|
||||
return result
|
@@ -1,254 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 .asset import Asset
|
||||
from . import utils
|
||||
from .partial_emoji import _EmojiTag
|
||||
from .user import User
|
||||
|
||||
class Emoji(_EmojiTag):
|
||||
"""Represents a custom emoji.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two emoji are the same.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two emoji are not the same.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the emoji's hash.
|
||||
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(field, value)`` pairs. This allows this class
|
||||
to be used as an iterable in list/dict/etc constructions.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the emoji rendered for discord.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the emoji.
|
||||
id: :class:`int`
|
||||
The emoji's ID.
|
||||
require_colons: :class:`bool`
|
||||
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
|
||||
animated: :class:`bool`
|
||||
Whether an emoji is animated or not.
|
||||
managed: :class:`bool`
|
||||
If this emoji is managed by a Twitch integration.
|
||||
guild_id: :class:`int`
|
||||
The guild ID the emoji belongs to.
|
||||
available: :class:`bool`
|
||||
Whether the emoji is available for use.
|
||||
user: Optional[:class:`User`]
|
||||
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
|
||||
having the :attr:`~Permissions.manage_emojis` permission.
|
||||
"""
|
||||
__slots__ = ('require_colons', 'animated', 'managed', 'id', 'name', '_roles', 'guild_id',
|
||||
'_state', 'user', 'available')
|
||||
|
||||
def __init__(self, *, guild, state, data):
|
||||
self.guild_id = guild.id
|
||||
self._state = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, emoji):
|
||||
self.require_colons = emoji['require_colons']
|
||||
self.managed = emoji['managed']
|
||||
self.id = int(emoji['id'])
|
||||
self.name = emoji['name']
|
||||
self.animated = emoji.get('animated', False)
|
||||
self.available = emoji.get('available', True)
|
||||
self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))
|
||||
user = emoji.get('user')
|
||||
self.user = User(state=self._state, data=user) if user else None
|
||||
|
||||
def _iterator(self):
|
||||
for attr in self.__slots__:
|
||||
if attr[0] != '_':
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None:
|
||||
yield (attr, value)
|
||||
|
||||
def __iter__(self):
|
||||
return self._iterator()
|
||||
|
||||
def __str__(self):
|
||||
if self.animated:
|
||||
return '<a:{0.name}:{0.id}>'.format(self)
|
||||
return "<:{0.name}:{0.id}>".format(self)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Emoji id={0.id} name={0.name!r} animated={0.animated} managed={0.managed}>'.format(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _EmojiTag) and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id >> 22
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`Asset`: Returns the asset of the emoji.
|
||||
|
||||
This is equivalent to calling :meth:`url_as` with
|
||||
the default parameters (i.e. png/gif detection).
|
||||
"""
|
||||
return self.url_as(format=None)
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
|
||||
|
||||
If roles is empty, the emoji is unrestricted.
|
||||
"""
|
||||
guild = self.guild
|
||||
if guild is None:
|
||||
return []
|
||||
|
||||
return [role for role in guild.roles if self._roles.has(role.id)]
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
""":class:`Guild`: The guild this emoji belongs to."""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
|
||||
def url_as(self, *, format=None, static_format="png"):
|
||||
"""Returns an :class:`Asset` for the emoji's url.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
||||
'gif' is only valid for animated emojis.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the emojis to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected as either 'gif' or static_format, depending on whether the
|
||||
emoji is animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated emoji's to.
|
||||
Defaults to 'png'
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or ``static_format``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
||||
|
||||
|
||||
def is_usable(self):
|
||||
""":class:`bool`: Whether the bot can use this emoji.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if not self.available:
|
||||
return False
|
||||
if not self._roles:
|
||||
return True
|
||||
emoji_roles, my_roles = self._roles, self.guild.me._roles
|
||||
return any(my_roles.has(role_id) for role_id in emoji_roles)
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Deletes the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for deleting this emoji. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to delete emojis.
|
||||
HTTPException
|
||||
An error occurred deleting the emoji.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
||||
|
||||
async def edit(self, *, name=None, roles=None, reason=None):
|
||||
r"""|coro|
|
||||
|
||||
Edits the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The new emoji name.
|
||||
roles: Optional[list[:class:`Role`]]
|
||||
A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this emoji. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to edit emojis.
|
||||
HTTPException
|
||||
An error occurred editing the emoji.
|
||||
"""
|
||||
|
||||
name = name or self.name
|
||||
if roles:
|
||||
roles = [role.id for role in roles]
|
||||
await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason)
|
@@ -1,471 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import types
|
||||
from collections import namedtuple
|
||||
|
||||
__all__ = (
|
||||
'Enum',
|
||||
'ChannelType',
|
||||
'MessageType',
|
||||
'VoiceRegion',
|
||||
'SpeakingState',
|
||||
'VerificationLevel',
|
||||
'ContentFilter',
|
||||
'Status',
|
||||
'DefaultAvatar',
|
||||
'RelationshipType',
|
||||
'AuditLogAction',
|
||||
'AuditLogActionCategory',
|
||||
'UserFlags',
|
||||
'ActivityType',
|
||||
'HypeSquadHouse',
|
||||
'NotificationLevel',
|
||||
'PremiumType',
|
||||
'UserContentFilter',
|
||||
'FriendFlags',
|
||||
'TeamMembershipState',
|
||||
'Theme',
|
||||
'WebhookType',
|
||||
'ExpireBehaviour',
|
||||
'ExpireBehavior',
|
||||
'StickerType',
|
||||
)
|
||||
|
||||
def _create_value_cls(name):
|
||||
cls = namedtuple('_EnumValue_' + name, 'name value')
|
||||
cls.__repr__ = lambda self: '<%s.%s: %r>' % (name, self.name, self.value)
|
||||
cls.__str__ = lambda self: '%s.%s' % (name, self.name)
|
||||
return cls
|
||||
|
||||
def _is_descriptor(obj):
|
||||
return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')
|
||||
|
||||
class EnumMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
value_mapping = {}
|
||||
member_mapping = {}
|
||||
member_names = []
|
||||
|
||||
value_cls = _create_value_cls(name)
|
||||
for key, value in list(attrs.items()):
|
||||
is_descriptor = _is_descriptor(value)
|
||||
if key[0] == '_' and not is_descriptor:
|
||||
continue
|
||||
|
||||
# Special case classmethod to just pass through
|
||||
if isinstance(value, classmethod):
|
||||
continue
|
||||
|
||||
if is_descriptor:
|
||||
setattr(value_cls, key, value)
|
||||
del attrs[key]
|
||||
continue
|
||||
|
||||
try:
|
||||
new_value = value_mapping[value]
|
||||
except KeyError:
|
||||
new_value = value_cls(name=key, value=value)
|
||||
value_mapping[value] = new_value
|
||||
member_names.append(key)
|
||||
|
||||
member_mapping[key] = new_value
|
||||
attrs[key] = new_value
|
||||
|
||||
attrs['_enum_value_map_'] = value_mapping
|
||||
attrs['_enum_member_map_'] = member_mapping
|
||||
attrs['_enum_member_names_'] = member_names
|
||||
actual_cls = super().__new__(cls, name, bases, attrs)
|
||||
value_cls._actual_enum_cls_ = actual_cls
|
||||
return actual_cls
|
||||
|
||||
def __iter__(cls):
|
||||
return (cls._enum_member_map_[name] for name in cls._enum_member_names_)
|
||||
|
||||
def __reversed__(cls):
|
||||
return (cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_))
|
||||
|
||||
def __len__(cls):
|
||||
return len(cls._enum_member_names_)
|
||||
|
||||
def __repr__(cls):
|
||||
return '<enum %r>' % cls.__name__
|
||||
|
||||
@property
|
||||
def __members__(cls):
|
||||
return types.MappingProxyType(cls._enum_member_map_)
|
||||
|
||||
def __call__(cls, value):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
|
||||
|
||||
def __getitem__(cls, key):
|
||||
return cls._enum_member_map_[key]
|
||||
|
||||
def __setattr__(cls, name, value):
|
||||
raise TypeError('Enums are immutable.')
|
||||
|
||||
def __delattr__(cls, attr):
|
||||
raise TypeError('Enums are immutable')
|
||||
|
||||
def __instancecheck__(self, instance):
|
||||
# isinstance(x, Y)
|
||||
# -> __instancecheck__(Y, x)
|
||||
try:
|
||||
return instance._actual_enum_cls_ is self
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
class Enum(metaclass=EnumMeta):
|
||||
@classmethod
|
||||
def try_value(cls, value):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
return value
|
||||
|
||||
|
||||
class ChannelType(Enum):
|
||||
text = 0
|
||||
private = 1
|
||||
voice = 2
|
||||
group = 3
|
||||
category = 4
|
||||
news = 5
|
||||
store = 6
|
||||
stage_voice = 13
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class MessageType(Enum):
|
||||
default = 0
|
||||
recipient_add = 1
|
||||
recipient_remove = 2
|
||||
call = 3
|
||||
channel_name_change = 4
|
||||
channel_icon_change = 5
|
||||
pins_add = 6
|
||||
new_member = 7
|
||||
premium_guild_subscription = 8
|
||||
premium_guild_tier_1 = 9
|
||||
premium_guild_tier_2 = 10
|
||||
premium_guild_tier_3 = 11
|
||||
channel_follow_add = 12
|
||||
guild_stream = 13
|
||||
guild_discovery_disqualified = 14
|
||||
guild_discovery_requalified = 15
|
||||
guild_discovery_grace_period_initial_warning = 16
|
||||
guild_discovery_grace_period_final_warning = 17
|
||||
|
||||
class VoiceRegion(Enum):
|
||||
us_west = 'us-west'
|
||||
us_east = 'us-east'
|
||||
us_south = 'us-south'
|
||||
us_central = 'us-central'
|
||||
eu_west = 'eu-west'
|
||||
eu_central = 'eu-central'
|
||||
singapore = 'singapore'
|
||||
london = 'london'
|
||||
sydney = 'sydney'
|
||||
amsterdam = 'amsterdam'
|
||||
frankfurt = 'frankfurt'
|
||||
brazil = 'brazil'
|
||||
hongkong = 'hongkong'
|
||||
russia = 'russia'
|
||||
japan = 'japan'
|
||||
southafrica = 'southafrica'
|
||||
south_korea = 'south-korea'
|
||||
india = 'india'
|
||||
europe = 'europe'
|
||||
dubai = 'dubai'
|
||||
vip_us_east = 'vip-us-east'
|
||||
vip_us_west = 'vip-us-west'
|
||||
vip_amsterdam = 'vip-amsterdam'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class SpeakingState(Enum):
|
||||
none = 0
|
||||
voice = 1
|
||||
soundshare = 2
|
||||
priority = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
class VerificationLevel(Enum):
|
||||
none = 0
|
||||
low = 1
|
||||
medium = 2
|
||||
high = 3
|
||||
table_flip = 3
|
||||
extreme = 4
|
||||
double_table_flip = 4
|
||||
very_high = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class ContentFilter(Enum):
|
||||
disabled = 0
|
||||
no_role = 1
|
||||
all_members = 2
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class UserContentFilter(Enum):
|
||||
disabled = 0
|
||||
friends = 1
|
||||
all_messages = 2
|
||||
|
||||
class FriendFlags(Enum):
|
||||
noone = 0
|
||||
mutual_guilds = 1
|
||||
mutual_friends = 2
|
||||
guild_and_friends = 3
|
||||
everyone = 4
|
||||
|
||||
class Theme(Enum):
|
||||
light = 'light'
|
||||
dark = 'dark'
|
||||
|
||||
class Status(Enum):
|
||||
online = 'online'
|
||||
offline = 'offline'
|
||||
idle = 'idle'
|
||||
dnd = 'dnd'
|
||||
do_not_disturb = 'dnd'
|
||||
invisible = 'invisible'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class DefaultAvatar(Enum):
|
||||
blurple = 0
|
||||
grey = 1
|
||||
gray = 1
|
||||
green = 2
|
||||
orange = 3
|
||||
red = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class RelationshipType(Enum):
|
||||
friend = 1
|
||||
blocked = 2
|
||||
incoming_request = 3
|
||||
outgoing_request = 4
|
||||
|
||||
class NotificationLevel(Enum):
|
||||
all_messages = 0
|
||||
only_mentions = 1
|
||||
|
||||
class AuditLogActionCategory(Enum):
|
||||
create = 1
|
||||
delete = 2
|
||||
update = 3
|
||||
|
||||
class AuditLogAction(Enum):
|
||||
guild_update = 1
|
||||
channel_create = 10
|
||||
channel_update = 11
|
||||
channel_delete = 12
|
||||
overwrite_create = 13
|
||||
overwrite_update = 14
|
||||
overwrite_delete = 15
|
||||
kick = 20
|
||||
member_prune = 21
|
||||
ban = 22
|
||||
unban = 23
|
||||
member_update = 24
|
||||
member_role_update = 25
|
||||
member_move = 26
|
||||
member_disconnect = 27
|
||||
bot_add = 28
|
||||
role_create = 30
|
||||
role_update = 31
|
||||
role_delete = 32
|
||||
invite_create = 40
|
||||
invite_update = 41
|
||||
invite_delete = 42
|
||||
webhook_create = 50
|
||||
webhook_update = 51
|
||||
webhook_delete = 52
|
||||
emoji_create = 60
|
||||
emoji_update = 61
|
||||
emoji_delete = 62
|
||||
message_delete = 72
|
||||
message_bulk_delete = 73
|
||||
message_pin = 74
|
||||
message_unpin = 75
|
||||
integration_create = 80
|
||||
integration_update = 81
|
||||
integration_delete = 82
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
lookup = {
|
||||
AuditLogAction.guild_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.channel_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.kick: None,
|
||||
AuditLogAction.member_prune: None,
|
||||
AuditLogAction.ban: None,
|
||||
AuditLogAction.unban: None,
|
||||
AuditLogAction.member_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_move: None,
|
||||
AuditLogAction.member_disconnect: None,
|
||||
AuditLogAction.bot_add: None,
|
||||
AuditLogAction.role_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.role_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.invite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.invite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.webhook_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.webhook_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.emoji_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.emoji_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_pin: None,
|
||||
AuditLogAction.message_unpin: None,
|
||||
AuditLogAction.integration_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.integration_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
|
||||
}
|
||||
return lookup[self]
|
||||
|
||||
@property
|
||||
def target_type(self):
|
||||
v = self.value
|
||||
if v == -1:
|
||||
return 'all'
|
||||
elif v < 10:
|
||||
return 'guild'
|
||||
elif v < 20:
|
||||
return 'channel'
|
||||
elif v < 30:
|
||||
return 'user'
|
||||
elif v < 40:
|
||||
return 'role'
|
||||
elif v < 50:
|
||||
return 'invite'
|
||||
elif v < 60:
|
||||
return 'webhook'
|
||||
elif v < 70:
|
||||
return 'emoji'
|
||||
elif v == 73:
|
||||
return 'channel'
|
||||
elif v < 80:
|
||||
return 'message'
|
||||
elif v < 90:
|
||||
return 'integration'
|
||||
|
||||
class UserFlags(Enum):
|
||||
staff = 1
|
||||
partner = 2
|
||||
hypesquad = 4
|
||||
bug_hunter = 8
|
||||
mfa_sms = 16
|
||||
premium_promo_dismissed = 32
|
||||
hypesquad_bravery = 64
|
||||
hypesquad_brilliance = 128
|
||||
hypesquad_balance = 256
|
||||
early_supporter = 512
|
||||
team_user = 1024
|
||||
system = 4096
|
||||
has_unread_urgent_messages = 8192
|
||||
bug_hunter_level_2 = 16384
|
||||
verified_bot = 65536
|
||||
verified_bot_developer = 131072
|
||||
|
||||
class ActivityType(Enum):
|
||||
unknown = -1
|
||||
playing = 0
|
||||
streaming = 1
|
||||
listening = 2
|
||||
watching = 3
|
||||
custom = 4
|
||||
competing = 5
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
class HypeSquadHouse(Enum):
|
||||
bravery = 1
|
||||
brilliance = 2
|
||||
balance = 3
|
||||
|
||||
class PremiumType(Enum):
|
||||
nitro_classic = 1
|
||||
nitro = 2
|
||||
|
||||
class TeamMembershipState(Enum):
|
||||
invited = 1
|
||||
accepted = 2
|
||||
|
||||
class WebhookType(Enum):
|
||||
incoming = 1
|
||||
channel_follower = 2
|
||||
|
||||
class ExpireBehaviour(Enum):
|
||||
remove_role = 0
|
||||
kick = 1
|
||||
|
||||
ExpireBehavior = ExpireBehaviour
|
||||
|
||||
class StickerType(Enum):
|
||||
png = 1
|
||||
apng = 2
|
||||
lottie = 3
|
||||
|
||||
def try_enum(cls, val):
|
||||
"""A function that tries to turn the value into enum ``cls``.
|
||||
|
||||
If it fails it returns the value instead.
|
||||
"""
|
||||
|
||||
try:
|
||||
return cls._enum_value_map_[val]
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
return val
|
@@ -1,201 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for discord.py
|
||||
|
||||
Ideally speaking, this could be caught to handle any exceptions thrown from this library.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ClientException(DiscordException):
|
||||
"""Exception that's thrown when an operation in the :class:`Client` fails.
|
||||
|
||||
These are usually for exceptions that happened due to user input.
|
||||
"""
|
||||
pass
|
||||
|
||||
class NoMoreItems(DiscordException):
|
||||
"""Exception that is thrown when an async iteration operation has no more
|
||||
items."""
|
||||
pass
|
||||
|
||||
class GatewayNotFound(DiscordException):
|
||||
"""An exception that is usually thrown when the gateway hub
|
||||
for the :class:`Client` websocket is not found."""
|
||||
def __init__(self):
|
||||
message = 'The gateway to connect to discord was not found.'
|
||||
super(GatewayNotFound, self).__init__(message)
|
||||
|
||||
def flatten_error_dict(d, key=''):
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
new_key = key + '.' + k if key else k
|
||||
|
||||
if isinstance(v, dict):
|
||||
try:
|
||||
_errors = v['_errors']
|
||||
except KeyError:
|
||||
items.extend(flatten_error_dict(v, new_key).items())
|
||||
else:
|
||||
items.append((new_key, ' '.join(x.get('message', '') for x in _errors)))
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
|
||||
return dict(items)
|
||||
|
||||
class HTTPException(DiscordException):
|
||||
"""Exception that's thrown when an HTTP request operation fails.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
response: :class:`aiohttp.ClientResponse`
|
||||
The response of the failed HTTP request. This is an
|
||||
instance of :class:`aiohttp.ClientResponse`. In some cases
|
||||
this could also be a :class:`requests.Response`.
|
||||
|
||||
text: :class:`str`
|
||||
The text of the error. Could be an empty string.
|
||||
status: :class:`int`
|
||||
The status code of the HTTP request.
|
||||
code: :class:`int`
|
||||
The Discord specific error code for the failure.
|
||||
"""
|
||||
|
||||
def __init__(self, response, message):
|
||||
self.response = response
|
||||
self.status = response.status
|
||||
if isinstance(message, dict):
|
||||
self.code = message.get('code', 0)
|
||||
base = message.get('message', '')
|
||||
errors = message.get('errors')
|
||||
if errors:
|
||||
errors = flatten_error_dict(errors)
|
||||
helpful = '\n'.join('In %s: %s' % t for t in errors.items())
|
||||
self.text = base + '\n' + helpful
|
||||
else:
|
||||
self.text = base
|
||||
else:
|
||||
self.text = message
|
||||
self.code = 0
|
||||
|
||||
fmt = '{0.status} {0.reason} (error code: {1})'
|
||||
if len(self.text):
|
||||
fmt += ': {2}'
|
||||
|
||||
super().__init__(fmt.format(self.response, self.code, self.text))
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""Exception that's thrown for when status code 403 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
pass
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""Exception that's thrown for when status code 404 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
pass
|
||||
|
||||
class DiscordServerError(HTTPException):
|
||||
"""Exception that's thrown for when a 500 range status code occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
pass
|
||||
|
||||
class InvalidData(ClientException):
|
||||
"""Exception that's raised when the library encounters unknown
|
||||
or invalid data from Discord.
|
||||
"""
|
||||
pass
|
||||
|
||||
class InvalidArgument(ClientException):
|
||||
"""Exception that's thrown when an argument to a function
|
||||
is invalid some way (e.g. wrong value or wrong type).
|
||||
|
||||
This could be considered the analogous of ``ValueError`` and
|
||||
``TypeError`` except inherited from :exc:`ClientException` and thus
|
||||
:exc:`DiscordException`.
|
||||
"""
|
||||
pass
|
||||
|
||||
class LoginFailure(ClientException):
|
||||
"""Exception that's thrown when the :meth:`Client.login` function
|
||||
fails to log you in from improper credentials or some other misc.
|
||||
failure.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ConnectionClosed(ClientException):
|
||||
"""Exception that's thrown when the gateway connection is
|
||||
closed for reasons that could not be handled internally.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
code: :class:`int`
|
||||
The close code of the websocket.
|
||||
reason: :class:`str`
|
||||
The reason provided for the closure.
|
||||
shard_id: Optional[:class:`int`]
|
||||
The shard ID that got closed if applicable.
|
||||
"""
|
||||
def __init__(self, socket, *, shard_id, code=None):
|
||||
# This exception is just the same exception except
|
||||
# reconfigured to subclass ClientException for users
|
||||
self.code = code or socket.close_code
|
||||
# aiohttp doesn't seem to consistently provide close reason
|
||||
self.reason = ''
|
||||
self.shard_id = shard_id
|
||||
super().__init__('Shard ID %s WebSocket closed with %s' % (self.shard_id, self.code))
|
||||
|
||||
class PrivilegedIntentsRequired(ClientException):
|
||||
"""Exception that's thrown when the gateway is requesting privileged intents
|
||||
but they're not ticked in the developer page yet.
|
||||
|
||||
Go to https://discord.com/developers/applications/ and enable the intents
|
||||
that are required. Currently these are as follows:
|
||||
|
||||
- :attr:`Intents.members`
|
||||
- :attr:`Intents.presences`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
shard_id: Optional[:class:`int`]
|
||||
The shard ID that got closed if applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, shard_id):
|
||||
self.shard_id = shard_id
|
||||
msg = 'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the ' \
|
||||
'developer portal. It is recommended to go to https://discord.com/developers/applications/ ' \
|
||||
'and explicitly enable the privileged intents within your application\'s page. If this is not ' \
|
||||
'possible, then consider disabling the privileged intents instead.'
|
||||
super().__init__(msg % shard_id)
|
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
discord.ext.commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
|
||||
from .context import Context
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .help import *
|
||||
from .converter import *
|
||||
from .cooldowns import *
|
||||
from .cog import *
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
# This is merely a tag type to avoid circular import issues.
|
||||
# Yes, this is a terrible solution but ultimately it is the only solution.
|
||||
class _BaseCommand:
|
||||
__slots__ = ()
|
File diff suppressed because it is too large
Load Diff
@@ -1,451 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import copy
|
||||
from ._types import _BaseCommand
|
||||
|
||||
__all__ = (
|
||||
'CogMeta',
|
||||
'Cog',
|
||||
)
|
||||
|
||||
class CogMeta(type):
|
||||
"""A metaclass for defining a cog.
|
||||
|
||||
Note that you should probably not use this directly. It is exposed
|
||||
purely for documentation purposes along with making custom metaclasses to intermix
|
||||
with other metaclasses such as the :class:`abc.ABCMeta` metaclass.
|
||||
|
||||
For example, to create an abstract cog mixin class, the following would be done.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
import abc
|
||||
|
||||
class CogABCMeta(commands.CogMeta, abc.ABCMeta):
|
||||
pass
|
||||
|
||||
class SomeMixin(metaclass=abc.ABCMeta):
|
||||
pass
|
||||
|
||||
class SomeCogMixin(SomeMixin, commands.Cog, metaclass=CogABCMeta):
|
||||
pass
|
||||
|
||||
.. note::
|
||||
|
||||
When passing an attribute of a metaclass that is documented below, note
|
||||
that you must pass it as a keyword-only argument to the class creation
|
||||
like the following example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
class MyCog(commands.Cog, name='My Cog'):
|
||||
pass
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The cog name. By default, it is the name of the class with no modification.
|
||||
description: :class:`str`
|
||||
The cog description. By default, it is the cleaned docstring of the class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
command_attrs: :class:`dict`
|
||||
A list of attributes to apply to every command inside this cog. The dictionary
|
||||
is passed into the :class:`Command` options at ``__init__``.
|
||||
If you specify attributes inside the command attribute in the class, it will
|
||||
override the one specified inside this attribute. For example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
class MyCog(commands.Cog, command_attrs=dict(hidden=True)):
|
||||
@commands.command()
|
||||
async def foo(self, ctx):
|
||||
pass # hidden -> True
|
||||
|
||||
@commands.command(hidden=False)
|
||||
async def bar(self, ctx):
|
||||
pass # hidden -> False
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, bases, attrs = args
|
||||
attrs['__cog_name__'] = kwargs.pop('name', name)
|
||||
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
|
||||
|
||||
description = kwargs.pop('description', None)
|
||||
if description is None:
|
||||
description = inspect.cleandoc(attrs.get('__doc__', ''))
|
||||
attrs['__cog_description__'] = description
|
||||
|
||||
commands = {}
|
||||
listeners = {}
|
||||
no_bot_cog = 'Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})'
|
||||
|
||||
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||
for base in reversed(new_cls.__mro__):
|
||||
for elem, value in base.__dict__.items():
|
||||
if elem in commands:
|
||||
del commands[elem]
|
||||
if elem in listeners:
|
||||
del listeners[elem]
|
||||
|
||||
is_static_method = isinstance(value, staticmethod)
|
||||
if is_static_method:
|
||||
value = value.__func__
|
||||
if isinstance(value, _BaseCommand):
|
||||
if is_static_method:
|
||||
raise TypeError('Command in method {0}.{1!r} must not be staticmethod.'.format(base, elem))
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
commands[elem] = value
|
||||
elif inspect.iscoroutinefunction(value):
|
||||
try:
|
||||
getattr(value, '__cog_listener__')
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
listeners[elem] = value
|
||||
|
||||
new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
|
||||
|
||||
listeners_as_list = []
|
||||
for listener in listeners.values():
|
||||
for listener_name in listener.__cog_listener_names__:
|
||||
# I use __name__ instead of just storing the value so I can inject
|
||||
# the self attribute when the time comes to add them to the bot
|
||||
listeners_as_list.append((listener_name, listener.__name__))
|
||||
|
||||
new_cls.__cog_listeners__ = listeners_as_list
|
||||
return new_cls
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args)
|
||||
|
||||
@classmethod
|
||||
def qualified_name(cls):
|
||||
return cls.__cog_name__
|
||||
|
||||
def _cog_special_method(func):
|
||||
func.__cog_special_method__ = None
|
||||
return func
|
||||
|
||||
class Cog(metaclass=CogMeta):
|
||||
"""The base class that all cogs must inherit from.
|
||||
|
||||
A cog is a collection of commands, listeners, and optional state to
|
||||
help group commands together. More information on them can be found on
|
||||
the :ref:`ext_commands_cogs` page.
|
||||
|
||||
When inheriting from this class, the options shown in :class:`CogMeta`
|
||||
are equally valid here.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# For issue 426, we need to store a copy of the command objects
|
||||
# since we modify them to inject `self` to them.
|
||||
# To do this, we need to interfere with the Cog creation process.
|
||||
self = super().__new__(cls)
|
||||
cmd_attrs = cls.__cog_settings__
|
||||
|
||||
# Either update the command with the cog provided defaults or copy it.
|
||||
self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__)
|
||||
|
||||
lookup = {
|
||||
cmd.qualified_name: cmd
|
||||
for cmd in self.__cog_commands__
|
||||
}
|
||||
|
||||
# Update the Command instances dynamically as well
|
||||
for command in self.__cog_commands__:
|
||||
setattr(self, command.callback.__name__, command)
|
||||
parent = command.parent
|
||||
if parent is not None:
|
||||
# Get the latest parent reference
|
||||
parent = lookup[parent.qualified_name]
|
||||
|
||||
# Update our parent's reference to our self
|
||||
parent.remove_command(command.name)
|
||||
parent.add_command(command)
|
||||
|
||||
return self
|
||||
|
||||
def get_commands(self):
|
||||
r"""
|
||||
Returns
|
||||
--------
|
||||
List[:class:`.Command`]
|
||||
A :class:`list` of :class:`.Command`\s that are
|
||||
defined inside this cog.
|
||||
|
||||
.. note::
|
||||
|
||||
This does not include subcommands.
|
||||
"""
|
||||
return [c for c in self.__cog_commands__ if c.parent is None]
|
||||
|
||||
@property
|
||||
def qualified_name(self):
|
||||
""":class:`str`: Returns the cog's specified name, not the class name."""
|
||||
return self.__cog_name__
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
""":class:`str`: Returns the cog's description, typically the cleaned docstring."""
|
||||
return self.__cog_description__
|
||||
|
||||
@description.setter
|
||||
def description(self, description):
|
||||
self.__cog_description__ = description
|
||||
|
||||
def walk_commands(self):
|
||||
"""An iterator that recursively walks through this cog's commands and subcommands.
|
||||
|
||||
Yields
|
||||
------
|
||||
Union[:class:`.Command`, :class:`.Group`]
|
||||
A command or group from the cog.
|
||||
"""
|
||||
from .core import GroupMixin
|
||||
for command in self.__cog_commands__:
|
||||
if command.parent is None:
|
||||
yield command
|
||||
if isinstance(command, GroupMixin):
|
||||
yield from command.walk_commands()
|
||||
|
||||
def get_listeners(self):
|
||||
"""Returns a :class:`list` of (name, function) listener pairs that are defined in this cog.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[Tuple[:class:`str`, :ref:`coroutine <coroutine>`]]
|
||||
The listeners defined in this cog.
|
||||
"""
|
||||
return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__]
|
||||
|
||||
@classmethod
|
||||
def _get_overridden_method(cls, method):
|
||||
"""Return None if the method is not overridden. Otherwise returns the overridden method."""
|
||||
return getattr(method.__func__, '__cog_special_method__', method)
|
||||
|
||||
@classmethod
|
||||
def listener(cls, name=None):
|
||||
"""A decorator that marks a function as a listener.
|
||||
|
||||
This is the cog equivalent of :meth:`.Bot.listen`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the event being listened to. If not provided, it
|
||||
defaults to the function's name.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
The function is not a coroutine function or a string was not passed as
|
||||
the name.
|
||||
"""
|
||||
|
||||
if name is not None and not isinstance(name, str):
|
||||
raise TypeError('Cog.listener expected str but received {0.__class__.__name__!r} instead.'.format(name))
|
||||
|
||||
def decorator(func):
|
||||
actual = func
|
||||
if isinstance(actual, staticmethod):
|
||||
actual = actual.__func__
|
||||
if not inspect.iscoroutinefunction(actual):
|
||||
raise TypeError('Listener function must be a coroutine function.')
|
||||
actual.__cog_listener__ = True
|
||||
to_assign = name or actual.__name__
|
||||
try:
|
||||
actual.__cog_listener_names__.append(to_assign)
|
||||
except AttributeError:
|
||||
actual.__cog_listener_names__ = [to_assign]
|
||||
# we have to return `func` instead of `actual` because
|
||||
# we need the type to be `staticmethod` for the metaclass
|
||||
# to pick it up but the metaclass unfurls the function and
|
||||
# thus the assignments need to be on the actual function
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the cog has an error handler.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
|
||||
|
||||
@_cog_special_method
|
||||
def cog_unload(self):
|
||||
"""A special method that is called when the cog gets removed.
|
||||
|
||||
This function **cannot** be a coroutine. It must be a regular
|
||||
function.
|
||||
|
||||
Subclasses must replace this if they want special unloading behaviour.
|
||||
"""
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
def bot_check_once(self, ctx):
|
||||
"""A special method that registers as a :meth:`.Bot.check_once`
|
||||
check.
|
||||
|
||||
This function **can** be a coroutine and must take a sole parameter,
|
||||
``ctx``, to represent the :class:`.Context`.
|
||||
"""
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
def bot_check(self, ctx):
|
||||
"""A special method that registers as a :meth:`.Bot.check`
|
||||
check.
|
||||
|
||||
This function **can** be a coroutine and must take a sole parameter,
|
||||
``ctx``, to represent the :class:`.Context`.
|
||||
"""
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
def cog_check(self, ctx):
|
||||
"""A special method that registers as a :func:`commands.check`
|
||||
for every command and subcommand in this cog.
|
||||
|
||||
This function **can** be a coroutine and must take a sole parameter,
|
||||
``ctx``, to represent the :class:`.Context`.
|
||||
"""
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_command_error(self, ctx, error):
|
||||
"""A special method that is called whenever an error
|
||||
is dispatched inside this cog.
|
||||
|
||||
This is similar to :func:`.on_command_error` except only applying
|
||||
to the commands inside this cog.
|
||||
|
||||
This **must** be a coroutine.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context where the error happened.
|
||||
error: :class:`CommandError`
|
||||
The error that happened.
|
||||
"""
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_before_invoke(self, ctx):
|
||||
"""A special method that acts as a cog local pre-invoke hook.
|
||||
|
||||
This is similar to :meth:`.Command.before_invoke`.
|
||||
|
||||
This **must** be a coroutine.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context.
|
||||
"""
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_after_invoke(self, ctx):
|
||||
"""A special method that acts as a cog local post-invoke hook.
|
||||
|
||||
This is similar to :meth:`.Command.after_invoke`.
|
||||
|
||||
This **must** be a coroutine.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _inject(self, bot):
|
||||
cls = self.__class__
|
||||
|
||||
# realistically, the only thing that can cause loading errors
|
||||
# is essentially just the command loading, which raises if there are
|
||||
# duplicates. When this condition is met, we want to undo all what
|
||||
# we've added so far for some form of atomic loading.
|
||||
for index, command in enumerate(self.__cog_commands__):
|
||||
command.cog = self
|
||||
if command.parent is None:
|
||||
try:
|
||||
bot.add_command(command)
|
||||
except Exception as e:
|
||||
# undo our additions
|
||||
for to_undo in self.__cog_commands__[:index]:
|
||||
if to_undo.parent is None:
|
||||
bot.remove_command(to_undo.name)
|
||||
raise e
|
||||
|
||||
# check if we're overriding the default
|
||||
if cls.bot_check is not Cog.bot_check:
|
||||
bot.add_check(self.bot_check)
|
||||
|
||||
if cls.bot_check_once is not Cog.bot_check_once:
|
||||
bot.add_check(self.bot_check_once, call_once=True)
|
||||
|
||||
# while Bot.add_listener can raise if it's not a coroutine,
|
||||
# this precondition is already met by the listener decorator
|
||||
# already, thus this should never raise.
|
||||
# Outside of, memory errors and the like...
|
||||
for name, method_name in self.__cog_listeners__:
|
||||
bot.add_listener(getattr(self, method_name), name)
|
||||
|
||||
return self
|
||||
|
||||
def _eject(self, bot):
|
||||
cls = self.__class__
|
||||
|
||||
try:
|
||||
for command in self.__cog_commands__:
|
||||
if command.parent is None:
|
||||
bot.remove_command(command.name)
|
||||
|
||||
for _, method_name in self.__cog_listeners__:
|
||||
bot.remove_listener(getattr(self, method_name))
|
||||
|
||||
if cls.bot_check is not Cog.bot_check:
|
||||
bot.remove_check(self.bot_check)
|
||||
|
||||
if cls.bot_check_once is not Cog.bot_check_once:
|
||||
bot.remove_check(self.bot_check_once, call_once=True)
|
||||
finally:
|
||||
try:
|
||||
self.cog_unload()
|
||||
except Exception:
|
||||
pass
|
@@ -1,340 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
r"""Represents the context in which a command is being invoked under.
|
||||
|
||||
This class contains a lot of meta data to help you understand more about
|
||||
the invocation context. This class is not created manually and is instead
|
||||
passed around to commands as the first parameter.
|
||||
|
||||
This class implements the :class:`~discord.abc.Messageable` ABC.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message: :class:`.Message`
|
||||
The message that triggered the command being executed.
|
||||
bot: :class:`.Bot`
|
||||
The bot that contains the command being executed.
|
||||
args: :class:`list`
|
||||
The list of transformed arguments that were passed into the command.
|
||||
If this is accessed during the :func:`on_command_error` event
|
||||
then this list could be incomplete.
|
||||
kwargs: :class:`dict`
|
||||
A dictionary of transformed arguments that were passed into the command.
|
||||
Similar to :attr:`args`\, if this is accessed in the
|
||||
:func:`on_command_error` event then this dict could be incomplete.
|
||||
prefix: :class:`str`
|
||||
The prefix that was used to invoke the command.
|
||||
command: :class:`Command`
|
||||
The command that is being invoked currently.
|
||||
invoked_with: :class:`str`
|
||||
The command name that triggered this invocation. Useful for finding out
|
||||
which alias called the command.
|
||||
invoked_parents: List[:class:`str`]
|
||||
The command names of the parents that triggered this invocation. Useful for
|
||||
finding out which aliases called the command.
|
||||
|
||||
For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
invoked_subcommand: :class:`Command`
|
||||
The subcommand that was invoked.
|
||||
If no valid subcommand was invoked then this is equal to ``None``.
|
||||
subcommand_passed: Optional[:class:`str`]
|
||||
The string that was attempted to call a subcommand. This does not have
|
||||
to point to a valid registered subcommand and could just point to a
|
||||
nonsense string. If nothing was passed to attempt a call to a
|
||||
subcommand then this is set to ``None``.
|
||||
command_failed: :class:`bool`
|
||||
A boolean that indicates if the command failed to be parsed, checked,
|
||||
or invoked.
|
||||
"""
|
||||
|
||||
def __init__(self, **attrs):
|
||||
self.message = attrs.pop('message', None)
|
||||
self.bot = attrs.pop('bot', None)
|
||||
self.args = attrs.pop('args', [])
|
||||
self.kwargs = attrs.pop('kwargs', {})
|
||||
self.prefix = attrs.pop('prefix')
|
||||
self.command = attrs.pop('command', None)
|
||||
self.view = attrs.pop('view', None)
|
||||
self.invoked_with = attrs.pop('invoked_with', None)
|
||||
self.invoked_parents = attrs.pop('invoked_parents', [])
|
||||
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
||||
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
||||
self.command_failed = attrs.pop('command_failed', False)
|
||||
self._state = self.message._state
|
||||
|
||||
async def invoke(self, *args, **kwargs):
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
|
||||
This is useful if you want to just call the callback that a
|
||||
:class:`.Command` holds internally.
|
||||
|
||||
.. note::
|
||||
|
||||
This does not handle converters, checks, cooldowns, pre-invoke,
|
||||
or after-invoke hooks in any matter. It calls the internal callback
|
||||
directly as-if it was a regular function.
|
||||
|
||||
You must take care in passing the proper arguments when
|
||||
using this function.
|
||||
|
||||
.. warning::
|
||||
|
||||
The first parameter passed **must** be the command being invoked.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`.Command`
|
||||
The command that is going to be called.
|
||||
\*args
|
||||
The arguments to to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The command argument to invoke is missing.
|
||||
"""
|
||||
|
||||
try:
|
||||
command = args[0]
|
||||
except IndexError:
|
||||
raise TypeError('Missing command to invoke.') from None
|
||||
|
||||
arguments = []
|
||||
if command.cog is not None:
|
||||
arguments.append(command.cog)
|
||||
|
||||
arguments.append(self)
|
||||
arguments.extend(args[1:])
|
||||
|
||||
ret = await command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
|
||||
async def reinvoke(self, *, call_hooks=False, restart=True):
|
||||
"""|coro|
|
||||
|
||||
Calls the command again.
|
||||
|
||||
This is similar to :meth:`~.Context.invoke` except that it bypasses
|
||||
checks, cooldowns, and error handlers.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to bypass :exc:`.UserInputError` derived exceptions,
|
||||
it is recommended to use the regular :meth:`~.Context.invoke`
|
||||
as it will work more naturally. After all, this will end up
|
||||
using the old arguments the user has used and will thus just
|
||||
fail again.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
call_hooks: :class:`bool`
|
||||
Whether to call the before and after invoke hooks.
|
||||
restart: :class:`bool`
|
||||
Whether to start the call chain from the very beginning
|
||||
or where we left off (i.e. the command that caused the error).
|
||||
The default is to start where we left off.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The context to reinvoke is not valid.
|
||||
"""
|
||||
cmd = self.command
|
||||
view = self.view
|
||||
if cmd is None:
|
||||
raise ValueError('This context is not valid.')
|
||||
|
||||
# some state to revert to when we're done
|
||||
index, previous = view.index, view.previous
|
||||
invoked_with = self.invoked_with
|
||||
invoked_subcommand = self.invoked_subcommand
|
||||
invoked_parents = self.invoked_parents
|
||||
subcommand_passed = self.subcommand_passed
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.previous = 0
|
||||
self.invoked_parents = []
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
try:
|
||||
await to_call.reinvoke(self, call_hooks=call_hooks)
|
||||
finally:
|
||||
self.command = cmd
|
||||
view.index = index
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.invoked_parents = invoked_parents
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
""":class:`bool`: Checks if the invocation context is valid to be invoked with."""
|
||||
return self.prefix is not None and self.command is not None
|
||||
|
||||
async def _get_channel(self):
|
||||
return self.channel
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
return None
|
||||
return self.command.cog
|
||||
|
||||
@discord.utils.cached_property
|
||||
def guild(self):
|
||||
"""Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available."""
|
||||
return self.message.guild
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Message.channel`.
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@discord.utils.cached_property
|
||||
def author(self):
|
||||
"""Union[:class:`~discord.User`, :class:`.Member`]:
|
||||
Returns the author associated with this context's command. Shorthand for :attr:`.Message.author`
|
||||
"""
|
||||
return self.message.author
|
||||
|
||||
@discord.utils.cached_property
|
||||
def me(self):
|
||||
"""Union[:class:`.Member`, :class:`.ClientUser`]:
|
||||
Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message contexts.
|
||||
"""
|
||||
return self.guild.me if self.guild is not None else self.bot.user
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
|
||||
g = self.guild
|
||||
return g.voice_client if g else None
|
||||
|
||||
async def send_help(self, *args):
|
||||
"""send_help(entity=<bot>)
|
||||
|
||||
|coro|
|
||||
|
||||
Shows the help command for the specified entity if given.
|
||||
The entity can be a command or a cog.
|
||||
|
||||
If no entity is given, then it'll show help for the
|
||||
entire bot.
|
||||
|
||||
If the entity is a string, then it looks up whether it's a
|
||||
:class:`Cog` or a :class:`Command`.
|
||||
|
||||
.. note::
|
||||
|
||||
Due to the way this function works, instead of returning
|
||||
something similar to :meth:`~.commands.HelpCommand.command_not_found`
|
||||
this returns :class:`None` on bad input or no help command.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]]
|
||||
The entity to show help for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Any
|
||||
The result of the help command, if any.
|
||||
"""
|
||||
from .core import Group, Command, wrap_callback
|
||||
from .errors import CommandError
|
||||
|
||||
bot = self.bot
|
||||
cmd = bot.help_command
|
||||
|
||||
if cmd is None:
|
||||
return None
|
||||
|
||||
cmd = cmd.copy()
|
||||
cmd.context = self
|
||||
if len(args) == 0:
|
||||
await cmd.prepare_help_command(self, None)
|
||||
mapping = cmd.get_bot_mapping()
|
||||
injected = wrap_callback(cmd.send_bot_help)
|
||||
try:
|
||||
return await injected(mapping)
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
return None
|
||||
|
||||
entity = args[0]
|
||||
if entity is None:
|
||||
return None
|
||||
|
||||
if isinstance(entity, str):
|
||||
entity = bot.get_cog(entity) or bot.get_command(entity)
|
||||
|
||||
try:
|
||||
entity.qualified_name
|
||||
except AttributeError:
|
||||
# if we're here then it's not a cog, group, or command.
|
||||
return None
|
||||
|
||||
await cmd.prepare_help_command(self, entity.qualified_name)
|
||||
|
||||
try:
|
||||
if hasattr(entity, '__cog_commands__'):
|
||||
injected = wrap_callback(cmd.send_cog_help)
|
||||
return await injected(entity)
|
||||
elif isinstance(entity, Group):
|
||||
injected = wrap_callback(cmd.send_group_help)
|
||||
return await injected(entity)
|
||||
elif isinstance(entity, Command):
|
||||
injected = wrap_callback(cmd.send_command_help)
|
||||
return await injected(entity)
|
||||
else:
|
||||
return None
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
|
||||
@discord.utils.copy_doc(discord.Message.reply)
|
||||
async def reply(self, content=None, **kwargs):
|
||||
return await self.message.reply(content, **kwargs)
|
@@ -1,852 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
from .errors import *
|
||||
|
||||
__all__ = (
|
||||
'Converter',
|
||||
'MemberConverter',
|
||||
'UserConverter',
|
||||
'MessageConverter',
|
||||
'PartialMessageConverter',
|
||||
'TextChannelConverter',
|
||||
'InviteConverter',
|
||||
'GuildConverter',
|
||||
'RoleConverter',
|
||||
'GameConverter',
|
||||
'ColourConverter',
|
||||
'ColorConverter',
|
||||
'VoiceChannelConverter',
|
||||
'StageChannelConverter',
|
||||
'EmojiConverter',
|
||||
'PartialEmojiConverter',
|
||||
'CategoryChannelConverter',
|
||||
'IDConverter',
|
||||
'StoreChannelConverter',
|
||||
'clean_content',
|
||||
'Greedy',
|
||||
)
|
||||
|
||||
def _get_from_guilds(bot, getter, argument):
|
||||
result = None
|
||||
for guild in bot.guilds:
|
||||
result = getattr(guild, getter)(argument)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
_utils_get = discord.utils.get
|
||||
|
||||
class Converter:
|
||||
"""The base class of custom converters that require the :class:`.Context`
|
||||
to be passed to be useful.
|
||||
|
||||
This allows you to implement converters that function similar to the
|
||||
special cased ``discord`` classes.
|
||||
|
||||
Classes that derive from this should override the :meth:`~.Converter.convert`
|
||||
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
"""|coro|
|
||||
|
||||
The method to override to do conversion logic.
|
||||
|
||||
If an error is found while converting, it is recommended to
|
||||
raise a :exc:`.CommandError` derived exception as it will
|
||||
properly propagate to the error handlers.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context that the argument is being used in.
|
||||
argument: :class:`str`
|
||||
The argument that is being converted.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.CommandError`
|
||||
A generic exception occurred when converting the argument.
|
||||
:exc:`.BadArgument`
|
||||
The converter failed to convert the argument.
|
||||
"""
|
||||
raise NotImplementedError('Derived classes need to implement this.')
|
||||
|
||||
class IDConverter(Converter):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r'([0-9]{15,20})$')
|
||||
super().__init__()
|
||||
|
||||
def _get_id_match(self, argument):
|
||||
return self._id_regex.match(argument)
|
||||
|
||||
class MemberConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Member`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
5. Lookup by nickname
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`
|
||||
|
||||
.. versionchanged:: 1.5.1
|
||||
This converter now lazily fetches members from the gateway and HTTP APIs,
|
||||
optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled.
|
||||
"""
|
||||
|
||||
async def query_member_named(self, guild, argument):
|
||||
cache = guild._state.member_cache_flags.joined
|
||||
if len(argument) > 5 and argument[-5] == '#':
|
||||
username, _, discriminator = argument.rpartition('#')
|
||||
members = await guild.query_members(username, limit=100, cache=cache)
|
||||
return discord.utils.get(members, name=username, discriminator=discriminator)
|
||||
else:
|
||||
members = await guild.query_members(argument, limit=100, cache=cache)
|
||||
return discord.utils.find(lambda m: m.name == argument or m.nick == argument, members)
|
||||
|
||||
async def query_member_by_id(self, bot, guild, user_id):
|
||||
ws = bot._get_websocket(shard_id=guild.shard_id)
|
||||
cache = guild._state.member_cache_flags.joined
|
||||
if ws.is_ratelimited():
|
||||
# If we're being rate limited on the WS, then fall back to using the HTTP API
|
||||
# So we don't have to wait ~60 seconds for the query to finish
|
||||
try:
|
||||
member = await guild.fetch_member(user_id)
|
||||
except discord.HTTPException:
|
||||
return None
|
||||
|
||||
if cache:
|
||||
guild._add_member(member)
|
||||
return member
|
||||
|
||||
# If we're not being rate limited then we can use the websocket to actually query
|
||||
members = await guild.query_members(limit=1, user_ids=[user_id], cache=cache)
|
||||
if not members:
|
||||
return None
|
||||
return members[0]
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
guild = ctx.guild
|
||||
result = None
|
||||
user_id = None
|
||||
if match is None:
|
||||
# not a mention...
|
||||
if guild:
|
||||
result = guild.get_member_named(argument)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_member_named', argument)
|
||||
else:
|
||||
user_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_member(user_id) or _utils_get(ctx.message.mentions, id=user_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_member', user_id)
|
||||
|
||||
if result is None:
|
||||
if guild is None:
|
||||
raise MemberNotFound(argument)
|
||||
|
||||
if user_id is not None:
|
||||
result = await self.query_member_by_id(bot, guild, user_id)
|
||||
else:
|
||||
result = await self.query_member_named(guild, argument)
|
||||
|
||||
if not result:
|
||||
raise MemberNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class UserConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.User`.
|
||||
|
||||
All lookups are via the global user cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This converter now lazily fetches users from the HTTP APIs if an ID is passed
|
||||
and it's not available in cache.
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
result = None
|
||||
state = ctx._state
|
||||
|
||||
if match is not None:
|
||||
user_id = int(match.group(1))
|
||||
result = ctx.bot.get_user(user_id) or _utils_get(ctx.message.mentions, id=user_id)
|
||||
if result is None:
|
||||
try:
|
||||
result = await ctx.bot.fetch_user(user_id)
|
||||
except discord.HTTPException:
|
||||
raise UserNotFound(argument) from None
|
||||
|
||||
return result
|
||||
|
||||
arg = argument
|
||||
|
||||
# Remove the '@' character if this is the first character from the argument
|
||||
if arg[0] == '@':
|
||||
# Remove first character
|
||||
arg = arg[1:]
|
||||
|
||||
# check for discriminator if it exists,
|
||||
if len(arg) > 5 and arg[-5] == '#':
|
||||
discrim = arg[-4:]
|
||||
name = arg[:-5]
|
||||
predicate = lambda u: u.name == name and u.discriminator == discrim
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
predicate = lambda u: u.name == arg
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
|
||||
if result is None:
|
||||
raise UserNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class PartialMessageConverter(Converter):
|
||||
"""Converts to a :class:`discord.PartialMessage`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
The creation strategy is as follows (in order):
|
||||
|
||||
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
|
||||
2. By message ID (The message is assumed to be in the context channel.)
|
||||
3. By message URL
|
||||
"""
|
||||
def _get_id_matches(self, argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,20}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,20})/(?P<message_id>[0-9]{15,20})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
channel_id = match.group("channel_id")
|
||||
return int(match.group("message_id")), int(channel_id) if channel_id else None
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
message_id, channel_id = self._get_id_matches(argument)
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
return discord.PartialMessage(channel=channel, id=message_id)
|
||||
|
||||
class MessageConverter(PartialMessageConverter):
|
||||
"""Converts to a :class:`discord.Message`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
|
||||
2. Lookup by message ID (the message **must** be in the context channel)
|
||||
3. Lookup by message URL
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
message_id, channel_id = self._get_id_matches(argument)
|
||||
message = ctx.bot._connection._get_message(message_id)
|
||||
if message:
|
||||
return message
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
try:
|
||||
return await channel.fetch_message(message_id)
|
||||
except discord.NotFound:
|
||||
raise MessageNotFound(argument)
|
||||
except discord.Forbidden:
|
||||
raise ChannelNotReadable(channel)
|
||||
|
||||
class TextChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.TextChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.text_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.TextChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.TextChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class VoiceChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.VoiceChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.voice_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.VoiceChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.VoiceChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class StageChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.StageChannel`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.stage_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StageChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StageChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.CategoryChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.categories, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.CategoryChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.CategoryChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class StoreChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.StoreChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StoreChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StoreChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter):
|
||||
"""Converts to a :class:`~discord.Colour`.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Add an alias named ColorConverter
|
||||
|
||||
The following formats are accepted:
|
||||
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- ``rgb(<number>, <number>, <number>)``
|
||||
- Any of the ``classmethod`` in :class:`Colour`
|
||||
|
||||
- The ``_`` in the name can be optionally replaced with spaces.
|
||||
|
||||
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).
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument`
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added support for ``rgb`` function and 3-digit hex shortcuts
|
||||
"""
|
||||
|
||||
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)')
|
||||
|
||||
def parse_hex_number(self, argument):
|
||||
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 BadColourArgument(argument)
|
||||
except ValueError:
|
||||
raise BadColourArgument(argument)
|
||||
else:
|
||||
return discord.Color(value=value)
|
||||
|
||||
def parse_rgb_number(self, argument, number):
|
||||
if number[-1] == '%':
|
||||
value = int(number[:-1])
|
||||
if not (0 <= value <= 100):
|
||||
raise BadColourArgument(argument)
|
||||
return round(255 * (value / 100))
|
||||
|
||||
value = int(number)
|
||||
if not (0 <= value <= 255):
|
||||
raise BadColourArgument(argument)
|
||||
return value
|
||||
|
||||
def parse_rgb(self, argument, *, regex=RGB_REGEX):
|
||||
match = regex.match(argument)
|
||||
if match is None:
|
||||
raise BadColourArgument(argument)
|
||||
|
||||
red = self.parse_rgb_number(argument, match.group('r'))
|
||||
green = self.parse_rgb_number(argument, match.group('g'))
|
||||
blue = self.parse_rgb_number(argument, match.group('b'))
|
||||
return discord.Color.from_rgb(red, green, blue)
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
if argument[0] == '#':
|
||||
return self.parse_hex_number(argument[1:])
|
||||
|
||||
if argument[0:2] == '0x':
|
||||
rest = argument[2:]
|
||||
# Legacy backwards compatible syntax
|
||||
if rest.startswith('#'):
|
||||
return self.parse_hex_number(rest[1:])
|
||||
return self.parse_hex_number(rest)
|
||||
|
||||
arg = argument.lower()
|
||||
if arg[0:3] == 'rgb':
|
||||
return self.parse_rgb(arg)
|
||||
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
|
||||
ColorConverter = ColourConverter
|
||||
|
||||
class RoleConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Role`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, the converter raises
|
||||
:exc:`.NoPrivateMessage` exception.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
guild = ctx.guild
|
||||
if not guild:
|
||||
raise NoPrivateMessage()
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument)
|
||||
if match:
|
||||
result = guild.get_role(int(match.group(1)))
|
||||
else:
|
||||
result = discord.utils.get(guild._roles.values(), name=argument)
|
||||
|
||||
if result is None:
|
||||
raise RoleNotFound(argument)
|
||||
return result
|
||||
|
||||
class GameConverter(Converter):
|
||||
"""Converts to :class:`~discord.Game`."""
|
||||
async def convert(self, ctx, argument):
|
||||
return discord.Game(name=argument)
|
||||
|
||||
class InviteConverter(Converter):
|
||||
"""Converts to a :class:`~discord.Invite`.
|
||||
|
||||
This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
try:
|
||||
invite = await ctx.bot.fetch_invite(argument)
|
||||
return invite
|
||||
except Exception as exc:
|
||||
raise BadInviteArgument() from exc
|
||||
|
||||
class GuildConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Guild`.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument)
|
||||
result = None
|
||||
|
||||
if match is not None:
|
||||
guild_id = int(match.group(1))
|
||||
result = ctx.bot.get_guild(guild_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||
|
||||
if result is None:
|
||||
raise GuildNotFound(argument)
|
||||
return result
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Emoji`.
|
||||
|
||||
All lookups are done for the local guild first, if available. If that lookup
|
||||
fails, then it checks the client's global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by extracting ID from the emoji.
|
||||
3. Lookup by name
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
|
||||
result = None
|
||||
bot = ctx.bot
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# Try to get the emoji by name. Try local guild first.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, name=argument)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, name=argument)
|
||||
else:
|
||||
emoji_id = int(match.group(1))
|
||||
|
||||
# Try to look up emoji by id.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
raise EmojiNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class PartialEmojiConverter(Converter):
|
||||
"""Converts to a :class:`~discord.PartialEmoji`.
|
||||
|
||||
This is done by extracting the animated flag, name and ID from the emoji.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
|
||||
|
||||
if match:
|
||||
emoji_animated = bool(match.group(1))
|
||||
emoji_name = match.group(2)
|
||||
emoji_id = int(match.group(3))
|
||||
|
||||
return discord.PartialEmoji.with_state(ctx.bot._connection, animated=emoji_animated, name=emoji_name,
|
||||
id=emoji_id)
|
||||
|
||||
raise PartialEmojiConversionFailure(argument)
|
||||
|
||||
class clean_content(Converter):
|
||||
"""Converts the argument to mention scrubbed version of
|
||||
said content.
|
||||
|
||||
This behaves similarly to :attr:`~discord.Message.clean_content`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
fix_channel_mentions: :class:`bool`
|
||||
Whether to clean channel mentions.
|
||||
use_nicknames: :class:`bool`
|
||||
Whether to use nicknames when transforming mentions.
|
||||
escape_markdown: :class:`bool`
|
||||
Whether to also escape special markdown characters.
|
||||
remove_markdown: :class:`bool`
|
||||
Whether to also remove special markdown characters. This option is not supported with ``escape_markdown``
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
|
||||
self.fix_channel_mentions = fix_channel_mentions
|
||||
self.use_nicknames = use_nicknames
|
||||
self.escape_markdown = escape_markdown
|
||||
self.remove_markdown = remove_markdown
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
message = ctx.message
|
||||
transformations = {}
|
||||
|
||||
if self.fix_channel_mentions and ctx.guild:
|
||||
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
||||
ch = _get(id)
|
||||
return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel')
|
||||
|
||||
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
||||
|
||||
if self.use_nicknames and ctx.guild:
|
||||
def resolve_member(id, *, _get=ctx.guild.get_member):
|
||||
m = _get(id)
|
||||
return '@' + m.display_name if m else '@deleted-user'
|
||||
else:
|
||||
def resolve_member(id, *, _get=ctx.bot.get_user):
|
||||
m = _get(id)
|
||||
return '@' + m.name if m else '@deleted-user'
|
||||
|
||||
|
||||
transformations.update(
|
||||
('<@%s>' % member_id, resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
transformations.update(
|
||||
('<@!%s>' % member_id, resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
if ctx.guild:
|
||||
def resolve_role(_id, *, _find=ctx.guild.get_role):
|
||||
r = _find(_id)
|
||||
return '@' + r.name if r else '@deleted-role'
|
||||
|
||||
transformations.update(
|
||||
('<@&%s>' % role_id, resolve_role(role_id))
|
||||
for role_id in message.raw_role_mentions
|
||||
)
|
||||
|
||||
def repl(obj):
|
||||
return transformations.get(obj.group(0), '')
|
||||
|
||||
pattern = re.compile('|'.join(transformations.keys()))
|
||||
result = pattern.sub(repl, argument)
|
||||
|
||||
if self.escape_markdown:
|
||||
result = discord.utils.escape_markdown(result)
|
||||
elif self.remove_markdown:
|
||||
result = discord.utils.remove_markdown(result)
|
||||
|
||||
# Completely ensure no mentions escape:
|
||||
return discord.utils.escape_mentions(result)
|
||||
|
||||
class _Greedy:
|
||||
__slots__ = ('converter',)
|
||||
|
||||
def __init__(self, *, converter=None):
|
||||
self.converter = converter
|
||||
|
||||
def __getitem__(self, params):
|
||||
if not isinstance(params, tuple):
|
||||
params = (params,)
|
||||
if len(params) != 1:
|
||||
raise TypeError('Greedy[...] only takes a single argument')
|
||||
converter = params[0]
|
||||
|
||||
if not (callable(converter) or isinstance(converter, Converter) or hasattr(converter, '__origin__')):
|
||||
raise TypeError('Greedy[...] expects a type or a Converter instance.')
|
||||
|
||||
if converter is str or converter is type(None) or converter is _Greedy:
|
||||
raise TypeError('Greedy[%s] is invalid.' % converter.__name__)
|
||||
|
||||
if getattr(converter, '__origin__', None) is typing.Union and type(None) in converter.__args__:
|
||||
raise TypeError('Greedy[%r] is invalid.' % converter)
|
||||
|
||||
return self.__class__(converter=converter)
|
||||
|
||||
Greedy = _Greedy()
|
@@ -1,295 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 discord.enums import Enum
|
||||
import time
|
||||
import asyncio
|
||||
from collections import deque
|
||||
|
||||
from ...abc import PrivateChannel
|
||||
from .errors import MaxConcurrencyReached
|
||||
|
||||
__all__ = (
|
||||
'BucketType',
|
||||
'Cooldown',
|
||||
'CooldownMapping',
|
||||
'MaxConcurrency',
|
||||
)
|
||||
|
||||
class BucketType(Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
guild = 2
|
||||
channel = 3
|
||||
member = 4
|
||||
category = 5
|
||||
role = 6
|
||||
|
||||
def get_key(self, msg):
|
||||
if self is BucketType.user:
|
||||
return msg.author.id
|
||||
elif self is BucketType.guild:
|
||||
return (msg.guild or msg.author).id
|
||||
elif self is BucketType.channel:
|
||||
return msg.channel.id
|
||||
elif self is BucketType.member:
|
||||
return ((msg.guild and msg.guild.id), msg.author.id)
|
||||
elif self is BucketType.category:
|
||||
return (msg.channel.category or msg.channel).id
|
||||
elif self is BucketType.role:
|
||||
# we return the channel id of a private-channel as there are only roles in guilds
|
||||
# and that yields the same result as for a guild with only the @everyone role
|
||||
# NOTE: PrivateChannel doesn't actually have an id attribute but we assume we are
|
||||
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
|
||||
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
|
||||
|
||||
def __call__(self, msg):
|
||||
return self.get_key(msg)
|
||||
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
||||
|
||||
def __init__(self, rate, per, type):
|
||||
self.rate = int(rate)
|
||||
self.per = float(per)
|
||||
self.type = type
|
||||
self._window = 0.0
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
if not callable(self.type):
|
||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
if not current:
|
||||
current = time.time()
|
||||
|
||||
tokens = self._tokens
|
||||
|
||||
if current > self._window + self.per:
|
||||
tokens = self.rate
|
||||
return tokens
|
||||
|
||||
def get_retry_after(self, current=None):
|
||||
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=None):
|
||||
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
|
||||
|
||||
# check if we are rate limited
|
||||
if self._tokens == 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
# we're not so decrement our tokens
|
||||
self._tokens -= 1
|
||||
|
||||
# see if we got rate limited due to this token change, and if
|
||||
# so update the window to point to our current time frame
|
||||
if self._tokens == 0:
|
||||
self._window = current
|
||||
|
||||
def reset(self):
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Cooldown(self.rate, self.per, self.type)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.format(self)
|
||||
|
||||
class CooldownMapping:
|
||||
def __init__(self, original):
|
||||
self._cache = {}
|
||||
self._cooldown = original
|
||||
|
||||
def copy(self):
|
||||
ret = CooldownMapping(self._cooldown)
|
||||
ret._cache = self._cache.copy()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self._cooldown is not None
|
||||
|
||||
@classmethod
|
||||
def from_cooldown(cls, rate, per, type):
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
return self._cooldown.type(msg)
|
||||
|
||||
def _verify_cache_integrity(self, current=None):
|
||||
# we want to delete all cache objects that haven't been used
|
||||
# in a cooldown window. e.g. if we have a command that has a
|
||||
# cooldown of 60s and it has not been used in 60s then that key should be deleted
|
||||
current = current or time.time()
|
||||
dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per]
|
||||
for k in dead_keys:
|
||||
del self._cache[k]
|
||||
|
||||
def get_bucket(self, message, current=None):
|
||||
if self._cooldown.type is BucketType.default:
|
||||
return self._cooldown
|
||||
|
||||
self._verify_cache_integrity(current)
|
||||
key = self._bucket_key(message)
|
||||
if key not in self._cache:
|
||||
bucket = self._cooldown.copy()
|
||||
self._cache[key] = bucket
|
||||
else:
|
||||
bucket = self._cache[key]
|
||||
|
||||
return bucket
|
||||
|
||||
def update_rate_limit(self, message, current=None):
|
||||
bucket = self.get_bucket(message, current)
|
||||
return bucket.update_rate_limit(current)
|
||||
|
||||
class _Semaphore:
|
||||
"""This class is a version of a semaphore.
|
||||
|
||||
If you're wondering why asyncio.Semaphore isn't being used,
|
||||
it's because it doesn't expose the internal value. This internal
|
||||
value is necessary because I need to support both `wait=True` and
|
||||
`wait=False`.
|
||||
|
||||
An asyncio.Queue could have been used to do this as well -- but it is
|
||||
not as inefficient since internally that uses two queues and is a bit
|
||||
overkill for what is basically a counter.
|
||||
"""
|
||||
|
||||
__slots__ = ('value', 'loop', '_waiters')
|
||||
|
||||
def __init__(self, number):
|
||||
self.value = number
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self._waiters = deque()
|
||||
|
||||
def __repr__(self):
|
||||
return '<_Semaphore value={0.value} waiters={1}>'.format(self, len(self._waiters))
|
||||
|
||||
def locked(self):
|
||||
return self.value == 0
|
||||
|
||||
def is_active(self):
|
||||
return len(self._waiters) > 0
|
||||
|
||||
def wake_up(self):
|
||||
while self._waiters:
|
||||
future = self._waiters.popleft()
|
||||
if not future.done():
|
||||
future.set_result(None)
|
||||
return
|
||||
|
||||
async def acquire(self, *, wait=False):
|
||||
if not wait and self.value <= 0:
|
||||
# signal that we're not acquiring
|
||||
return False
|
||||
|
||||
while self.value <= 0:
|
||||
future = self.loop.create_future()
|
||||
self._waiters.append(future)
|
||||
try:
|
||||
await future
|
||||
except:
|
||||
future.cancel()
|
||||
if self.value > 0 and not future.cancelled():
|
||||
self.wake_up()
|
||||
raise
|
||||
|
||||
self.value -= 1
|
||||
return True
|
||||
|
||||
def release(self):
|
||||
self.value += 1
|
||||
self.wake_up()
|
||||
|
||||
class MaxConcurrency:
|
||||
__slots__ = ('number', 'per', 'wait', '_mapping')
|
||||
|
||||
def __init__(self, number, *, per, wait):
|
||||
self._mapping = {}
|
||||
self.per = per
|
||||
self.number = number
|
||||
self.wait = wait
|
||||
|
||||
if number <= 0:
|
||||
raise ValueError('max_concurrency \'number\' cannot be less than 1')
|
||||
|
||||
if not isinstance(per, BucketType):
|
||||
raise TypeError('max_concurrency \'per\' must be of type BucketType not %r' % type(per))
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self.number, per=self.per, wait=self.wait)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MaxConcurrency per={0.per!r} number={0.number} wait={0.wait}>'.format(self)
|
||||
|
||||
def get_key(self, message):
|
||||
return self.per.get_key(message)
|
||||
|
||||
async def acquire(self, message):
|
||||
key = self.get_key(message)
|
||||
|
||||
try:
|
||||
sem = self._mapping[key]
|
||||
except KeyError:
|
||||
self._mapping[key] = sem = _Semaphore(self.number)
|
||||
|
||||
acquired = await sem.acquire(wait=self.wait)
|
||||
if not acquired:
|
||||
raise MaxConcurrencyReached(self.number, self.per)
|
||||
|
||||
async def release(self, message):
|
||||
# Technically there's no reason for this function to be async
|
||||
# But it might be more useful in the future
|
||||
key = self.get_key(message)
|
||||
|
||||
try:
|
||||
sem = self._mapping[key]
|
||||
except KeyError:
|
||||
# ...? peculiar
|
||||
return
|
||||
else:
|
||||
sem.release()
|
||||
|
||||
if sem.value >= self.number and not sem.is_active():
|
||||
del self._mapping[key]
|
File diff suppressed because it is too large
Load Diff
@@ -1,811 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 discord.errors import ClientException, DiscordException
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CommandError',
|
||||
'MissingRequiredArgument',
|
||||
'BadArgument',
|
||||
'PrivateMessageOnly',
|
||||
'NoPrivateMessage',
|
||||
'CheckFailure',
|
||||
'CheckAnyFailure',
|
||||
'CommandNotFound',
|
||||
'DisabledCommand',
|
||||
'CommandInvokeError',
|
||||
'TooManyArguments',
|
||||
'UserInputError',
|
||||
'CommandOnCooldown',
|
||||
'MaxConcurrencyReached',
|
||||
'NotOwner',
|
||||
'MessageNotFound',
|
||||
'MemberNotFound',
|
||||
'GuildNotFound',
|
||||
'UserNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelNotReadable',
|
||||
'BadColourArgument',
|
||||
'RoleNotFound',
|
||||
'BadInviteArgument',
|
||||
'EmojiNotFound',
|
||||
'PartialEmojiConversionFailure',
|
||||
'BadBoolArgument',
|
||||
'MissingRole',
|
||||
'BotMissingRole',
|
||||
'MissingAnyRole',
|
||||
'BotMissingAnyRole',
|
||||
'MissingPermissions',
|
||||
'BotMissingPermissions',
|
||||
'NSFWChannelRequired',
|
||||
'ConversionError',
|
||||
'BadUnionArgument',
|
||||
'ArgumentParsingError',
|
||||
'UnexpectedQuoteError',
|
||||
'InvalidEndOfQuotedStringError',
|
||||
'ExpectedClosingQuoteError',
|
||||
'ExtensionError',
|
||||
'ExtensionAlreadyLoaded',
|
||||
'ExtensionNotLoaded',
|
||||
'NoEntryPointError',
|
||||
'ExtensionFailed',
|
||||
'ExtensionNotFound',
|
||||
'CommandRegistrationError',
|
||||
)
|
||||
|
||||
class CommandError(DiscordException):
|
||||
r"""The base exception type for all 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 a special event
|
||||
from :class:`.Bot`\, :func:`on_command_error`.
|
||||
"""
|
||||
def __init__(self, message=None, *args):
|
||||
if message is not None:
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
|
||||
super().__init__(m, *args)
|
||||
else:
|
||||
super().__init__(*args)
|
||||
|
||||
class ConversionError(CommandError):
|
||||
"""Exception raised when a Converter class raises non-CommandError.
|
||||
|
||||
This inherits from :exc:`CommandError`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
converter: :class:`discord.ext.commands.Converter`
|
||||
The converter that failed.
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
def __init__(self, converter, original):
|
||||
self.converter = converter
|
||||
self.original = original
|
||||
|
||||
class UserInputError(CommandError):
|
||||
"""The base exception type for errors that involve errors
|
||||
regarding user input.
|
||||
|
||||
This inherits from :exc:`CommandError`.
|
||||
"""
|
||||
pass
|
||||
|
||||
class CommandNotFound(CommandError):
|
||||
"""Exception raised when a command is attempted to be invoked
|
||||
but no command under that name is found.
|
||||
|
||||
This is not raised for invalid subcommands, rather just the
|
||||
initial main command that is attempted to be invoked.
|
||||
|
||||
This inherits from :exc:`CommandError`.
|
||||
"""
|
||||
pass
|
||||
|
||||
class MissingRequiredArgument(UserInputError):
|
||||
"""Exception raised when parsing a command and a parameter
|
||||
that is required is not encountered.
|
||||
|
||||
This inherits from :exc:`UserInputError`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
param: :class:`inspect.Parameter`
|
||||
The argument that is missing.
|
||||
"""
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
super().__init__('{0.name} is a required argument that is missing.'.format(param))
|
||||
|
||||
class TooManyArguments(UserInputError):
|
||||
"""Exception raised when the command was passed too many arguments and its
|
||||
:attr:`.Command.ignore_extra` attribute was not set to ``True``.
|
||||
|
||||
This inherits from :exc:`UserInputError`
|
||||
"""
|
||||
pass
|
||||
|
||||
class BadArgument(UserInputError):
|
||||
"""Exception raised when a parsing or conversion failure is encountered
|
||||
on an argument to pass into a command.
|
||||
|
||||
This inherits from :exc:`UserInputError`
|
||||
"""
|
||||
pass
|
||||
|
||||
class CheckFailure(CommandError):
|
||||
"""Exception raised when the predicates in :attr:`.Command.checks` have failed.
|
||||
|
||||
This inherits from :exc:`CommandError`
|
||||
"""
|
||||
pass
|
||||
|
||||
class CheckAnyFailure(CheckFailure):
|
||||
"""Exception raised when all predicates in :func:`check_any` fail.
|
||||
|
||||
This inherits from :exc:`CheckFailure`.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
Attributes
|
||||
------------
|
||||
errors: List[:class:`CheckFailure`]
|
||||
A list of errors that were caught during execution.
|
||||
checks: List[Callable[[:class:`Context`], :class:`bool`]]
|
||||
A list of check predicates that failed.
|
||||
"""
|
||||
|
||||
def __init__(self, checks, errors):
|
||||
self.checks = checks
|
||||
self.errors = errors
|
||||
super().__init__('You do not have permission to run this command.')
|
||||
|
||||
class PrivateMessageOnly(CheckFailure):
|
||||
"""Exception raised when an operation does not work outside of private
|
||||
message contexts.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
"""
|
||||
def __init__(self, message=None):
|
||||
super().__init__(message or 'This command can only be used in private messages.')
|
||||
|
||||
class NoPrivateMessage(CheckFailure):
|
||||
"""Exception raised when an operation does not work in private message
|
||||
contexts.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
"""
|
||||
|
||||
def __init__(self, message=None):
|
||||
super().__init__(message or 'This command cannot be used in private messages.')
|
||||
|
||||
class NotOwner(CheckFailure):
|
||||
"""Exception raised when the message author is not the owner of the bot.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
"""
|
||||
pass
|
||||
|
||||
class MemberNotFound(BadArgument):
|
||||
"""Exception raised when the member provided was not found in the bot's
|
||||
cache.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The member supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Member "{}" not found.'.format(argument))
|
||||
|
||||
class GuildNotFound(BadArgument):
|
||||
"""Exception raised when the guild provided was not found in the bot's cache.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The guild supplied by the called that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Guild "{}" not found.'.format(argument))
|
||||
|
||||
class UserNotFound(BadArgument):
|
||||
"""Exception raised when the user provided was not found in the bot's
|
||||
cache.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The user supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('User "{}" not found.'.format(argument))
|
||||
|
||||
class MessageNotFound(BadArgument):
|
||||
"""Exception raised when the message provided was not found in the channel.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The message supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Message "{}" not found.'.format(argument))
|
||||
|
||||
class ChannelNotReadable(BadArgument):
|
||||
"""Exception raised when the bot does not have permission to read messages
|
||||
in the channel.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`.abc.GuildChannel`
|
||||
The channel supplied by the caller that was not readable
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__("Can't read messages in {}.".format(argument.mention))
|
||||
|
||||
class ChannelNotFound(BadArgument):
|
||||
"""Exception raised when the bot can not find the channel.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The channel supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Channel "{}" not found.'.format(argument))
|
||||
|
||||
class BadColourArgument(BadArgument):
|
||||
"""Exception raised when the colour is not valid.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The colour supplied by the caller that was not valid
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Colour "{}" is invalid.'.format(argument))
|
||||
|
||||
BadColorArgument = BadColourArgument
|
||||
|
||||
class RoleNotFound(BadArgument):
|
||||
"""Exception raised when the bot can not find the role.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The role supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Role "{}" not found.'.format(argument))
|
||||
|
||||
class BadInviteArgument(BadArgument):
|
||||
"""Exception raised when the invite is invalid or expired.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__('Invite is invalid or expired.')
|
||||
|
||||
class EmojiNotFound(BadArgument):
|
||||
"""Exception raised when the bot can not find the emoji.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The emoji supplied by the caller that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Emoji "{}" not found.'.format(argument))
|
||||
|
||||
class PartialEmojiConversionFailure(BadArgument):
|
||||
"""Exception raised when the emoji provided does not match the correct
|
||||
format.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The emoji supplied by the caller that did not match the regex
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
||||
|
||||
class BadBoolArgument(BadArgument):
|
||||
"""Exception raised when a boolean argument was not convertable.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The boolean argument supplied by the caller that is not in the predefined list
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('{} is not a recognised boolean option'.format(argument))
|
||||
|
||||
class DisabledCommand(CommandError):
|
||||
"""Exception raised when the command being invoked is disabled.
|
||||
|
||||
This inherits from :exc:`CommandError`
|
||||
"""
|
||||
pass
|
||||
|
||||
class CommandInvokeError(CommandError):
|
||||
"""Exception raised when the command being invoked raised an exception.
|
||||
|
||||
This inherits from :exc:`CommandError`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
def __init__(self, e):
|
||||
self.original = e
|
||||
super().__init__('Command raised an exception: {0.__class__.__name__}: {0}'.format(e))
|
||||
|
||||
class CommandOnCooldown(CommandError):
|
||||
"""Exception raised when the command being invoked is on cooldown.
|
||||
|
||||
This inherits from :exc:`CommandError`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
cooldown: Cooldown
|
||||
A class with attributes ``rate``, ``per``, and ``type`` similar to
|
||||
the :func:`.cooldown` decorator.
|
||||
retry_after: :class:`float`
|
||||
The amount of seconds to wait before you can retry again.
|
||||
"""
|
||||
def __init__(self, cooldown, retry_after):
|
||||
self.cooldown = cooldown
|
||||
self.retry_after = retry_after
|
||||
super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after))
|
||||
|
||||
class MaxConcurrencyReached(CommandError):
|
||||
"""Exception raised when the command being invoked has reached its maximum concurrency.
|
||||
|
||||
This inherits from :exc:`CommandError`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
number: :class:`int`
|
||||
The maximum number of concurrent invokers allowed.
|
||||
per: :class:`.BucketType`
|
||||
The bucket type passed to the :func:`.max_concurrency` decorator.
|
||||
"""
|
||||
|
||||
def __init__(self, number, per):
|
||||
self.number = number
|
||||
self.per = per
|
||||
name = per.name
|
||||
suffix = 'per %s' % name if per.name != 'default' else 'globally'
|
||||
plural = '%s times %s' if number > 1 else '%s time %s'
|
||||
fmt = plural % (number, suffix)
|
||||
super().__init__('Too many people using this command. It can only be used {} concurrently.'.format(fmt))
|
||||
|
||||
class MissingRole(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks a role to run a command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_role: Union[:class:`str`, :class:`int`]
|
||||
The required role that is missing.
|
||||
This is the parameter passed to :func:`~.commands.has_role`.
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Role {0!r} is required to run this command.'.format(missing_role)
|
||||
super().__init__(message)
|
||||
|
||||
class BotMissingRole(CheckFailure):
|
||||
"""Exception raised when the bot's member lacks a role to run a command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_role: Union[:class:`str`, :class:`int`]
|
||||
The required role that is missing.
|
||||
This is the parameter passed to :func:`~.commands.has_role`.
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Bot requires the role {0!r} to run this command'.format(missing_role)
|
||||
super().__init__(message)
|
||||
|
||||
class MissingAnyRole(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks any of
|
||||
the roles specified to run a command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_roles: List[Union[:class:`str`, :class:`int`]]
|
||||
The roles that the invoker is missing.
|
||||
These are the parameters passed to :func:`~.commands.has_any_role`.
|
||||
"""
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' or '.join(missing)
|
||||
|
||||
message = "You are missing at least one of the required roles: {}".format(fmt)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class BotMissingAnyRole(CheckFailure):
|
||||
"""Exception raised when the bot's member lacks any of
|
||||
the roles specified to run a command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_roles: List[Union[:class:`str`, :class:`int`]]
|
||||
The roles that the bot's member is missing.
|
||||
These are the parameters passed to :func:`~.commands.has_any_role`.
|
||||
|
||||
"""
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' or '.join(missing)
|
||||
|
||||
message = "Bot is missing at least one of the required roles: {}".format(fmt)
|
||||
super().__init__(message)
|
||||
|
||||
class NSFWChannelRequired(CheckFailure):
|
||||
"""Exception raised when a channel does not have the required NSFW setting.
|
||||
|
||||
This inherits from :exc:`CheckFailure`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
channel: :class:`discord.abc.GuildChannel`
|
||||
The channel that does not have NSFW enabled.
|
||||
"""
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
super().__init__("Channel '{}' needs to be NSFW for this command to work.".format(channel))
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks permissions to run a
|
||||
command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: :class:`list`
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'You are missing {} permission(s) to run this command.'.format(fmt)
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
"""Exception raised when the bot's member lacks permissions to run a
|
||||
command.
|
||||
|
||||
This inherits from :exc:`CheckFailure`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: :class:`list`
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'Bot requires {} permission(s) to run this command.'.format(fmt)
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BadUnionArgument(UserInputError):
|
||||
"""Exception raised when a :data:`typing.Union` converter fails for all
|
||||
its associated types.
|
||||
|
||||
This inherits from :exc:`UserInputError`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
param: :class:`inspect.Parameter`
|
||||
The parameter that failed being converted.
|
||||
converters: Tuple[Type, ...]
|
||||
A tuple of converters attempted in conversion, in order of failure.
|
||||
errors: List[:class:`CommandError`]
|
||||
A list of errors that were caught from failing the conversion.
|
||||
"""
|
||||
def __init__(self, param, converters, errors):
|
||||
self.param = param
|
||||
self.converters = converters
|
||||
self.errors = errors
|
||||
|
||||
def _get_name(x):
|
||||
try:
|
||||
return x.__name__
|
||||
except AttributeError:
|
||||
return x.__class__.__name__
|
||||
|
||||
to_string = [_get_name(x) for x in converters]
|
||||
if len(to_string) > 2:
|
||||
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
|
||||
else:
|
||||
fmt = ' or '.join(to_string)
|
||||
|
||||
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
|
||||
|
||||
class ArgumentParsingError(UserInputError):
|
||||
"""An exception raised when the parser fails to parse a user's input.
|
||||
|
||||
This inherits from :exc:`UserInputError`.
|
||||
|
||||
There are child classes that implement more granular parsing errors for
|
||||
i18n purposes.
|
||||
"""
|
||||
pass
|
||||
|
||||
class UnexpectedQuoteError(ArgumentParsingError):
|
||||
"""An exception raised when the parser encounters a quote mark inside a non-quoted string.
|
||||
|
||||
This inherits from :exc:`ArgumentParsingError`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
quote: :class:`str`
|
||||
The quote mark that was found inside the non-quoted string.
|
||||
"""
|
||||
def __init__(self, quote):
|
||||
self.quote = quote
|
||||
super().__init__('Unexpected quote mark, {0!r}, in non-quoted string'.format(quote))
|
||||
|
||||
class InvalidEndOfQuotedStringError(ArgumentParsingError):
|
||||
"""An exception raised when a space is expected after the closing quote in a string
|
||||
but a different character is found.
|
||||
|
||||
This inherits from :exc:`ArgumentParsingError`.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
char: :class:`str`
|
||||
The character found instead of the expected string.
|
||||
"""
|
||||
def __init__(self, char):
|
||||
self.char = char
|
||||
super().__init__('Expected space after closing quotation but received {0!r}'.format(char))
|
||||
|
||||
class ExpectedClosingQuoteError(ArgumentParsingError):
|
||||
"""An exception raised when a quote character is expected but not found.
|
||||
|
||||
This inherits from :exc:`ArgumentParsingError`.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
close_quote: :class:`str`
|
||||
The quote character expected.
|
||||
"""
|
||||
|
||||
def __init__(self, close_quote):
|
||||
self.close_quote = close_quote
|
||||
super().__init__('Expected closing {}.'.format(close_quote))
|
||||
|
||||
class ExtensionError(DiscordException):
|
||||
"""Base exception for extension related errors.
|
||||
|
||||
This inherits from :exc:`~discord.DiscordException`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
The extension that had an error.
|
||||
"""
|
||||
def __init__(self, message=None, *args, name):
|
||||
self.name = name
|
||||
message = message or 'Extension {!r} had an error.'.format(name)
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
|
||||
super().__init__(m, *args)
|
||||
|
||||
class ExtensionAlreadyLoaded(ExtensionError):
|
||||
"""An exception raised when an extension has already been loaded.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} is already loaded.'.format(name), name=name)
|
||||
|
||||
class ExtensionNotLoaded(ExtensionError):
|
||||
"""An exception raised when an extension was not loaded.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} has not been loaded.'.format(name), name=name)
|
||||
|
||||
class NoEntryPointError(ExtensionError):
|
||||
"""An exception raised when an extension does not have a ``setup`` entry point function.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name)
|
||||
|
||||
class ExtensionFailed(ExtensionError):
|
||||
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The extension that had the error.
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
def __init__(self, name, original):
|
||||
self.original = original
|
||||
fmt = 'Extension {0!r} raised an error: {1.__class__.__name__}: {1}'
|
||||
super().__init__(fmt.format(name, original), name=name)
|
||||
|
||||
class ExtensionNotFound(ExtensionError):
|
||||
"""An exception raised when an extension is not found.
|
||||
|
||||
This inherits from :exc:`ExtensionError`
|
||||
|
||||
.. versionchanged:: 1.3
|
||||
Made the ``original`` attribute always None.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The extension that had the error.
|
||||
original: :class:`NoneType`
|
||||
Always ``None`` for backwards compatibility.
|
||||
"""
|
||||
def __init__(self, name, original=None):
|
||||
self.original = None
|
||||
fmt = 'Extension {0!r} could not be loaded.'
|
||||
super().__init__(fmt.format(name), name=name)
|
||||
|
||||
class CommandRegistrationError(ClientException):
|
||||
"""An exception raised when the command can't be added
|
||||
because the name is already taken by a different command.
|
||||
|
||||
This inherits from :exc:`discord.ClientException`
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The command name that had the error.
|
||||
alias_conflict: :class:`bool`
|
||||
Whether the name that conflicts is an alias of the command we try to add.
|
||||
"""
|
||||
def __init__(self, name, *, alias_conflict=False):
|
||||
self.name = name
|
||||
self.alias_conflict = alias_conflict
|
||||
type_ = 'alias' if alias_conflict else 'command'
|
||||
super().__init__('The {} {} is already an existing command or alias.'.format(type_, name))
|
File diff suppressed because it is too large
Load Diff
@@ -1,194 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, ExpectedClosingQuoteError
|
||||
|
||||
# map from opening quotes to closing quotes
|
||||
_quotes = {
|
||||
'"': '"',
|
||||
"‘": "’",
|
||||
"‚": "‛",
|
||||
"“": "”",
|
||||
"„": "‟",
|
||||
"⹂": "⹂",
|
||||
"「": "」",
|
||||
"『": "』",
|
||||
"〝": "〞",
|
||||
"﹁": "﹂",
|
||||
"﹃": "﹄",
|
||||
""": """,
|
||||
"「": "」",
|
||||
"«": "»",
|
||||
"‹": "›",
|
||||
"《": "》",
|
||||
"〈": "〉",
|
||||
}
|
||||
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
|
||||
|
||||
class StringView:
|
||||
def __init__(self, buffer):
|
||||
self.index = 0
|
||||
self.buffer = buffer
|
||||
self.end = len(buffer)
|
||||
self.previous = 0
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
return None if self.eof else self.buffer[self.index]
|
||||
|
||||
@property
|
||||
def eof(self):
|
||||
return self.index >= self.end
|
||||
|
||||
def undo(self):
|
||||
self.index = self.previous
|
||||
|
||||
def skip_ws(self):
|
||||
pos = 0
|
||||
while not self.eof:
|
||||
try:
|
||||
current = self.buffer[self.index + pos]
|
||||
if not current.isspace():
|
||||
break
|
||||
pos += 1
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
self.previous = self.index
|
||||
self.index += pos
|
||||
return self.previous != self.index
|
||||
|
||||
def skip_string(self, string):
|
||||
strlen = len(string)
|
||||
if self.buffer[self.index:self.index + strlen] == string:
|
||||
self.previous = self.index
|
||||
self.index += strlen
|
||||
return True
|
||||
return False
|
||||
|
||||
def read_rest(self):
|
||||
result = self.buffer[self.index:]
|
||||
self.previous = self.index
|
||||
self.index = self.end
|
||||
return result
|
||||
|
||||
def read(self, n):
|
||||
result = self.buffer[self.index:self.index + n]
|
||||
self.previous = self.index
|
||||
self.index += n
|
||||
return result
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
result = self.buffer[self.index + 1]
|
||||
except IndexError:
|
||||
result = None
|
||||
|
||||
self.previous = self.index
|
||||
self.index += 1
|
||||
return result
|
||||
|
||||
def get_word(self):
|
||||
pos = 0
|
||||
while not self.eof:
|
||||
try:
|
||||
current = self.buffer[self.index + pos]
|
||||
if current.isspace():
|
||||
break
|
||||
pos += 1
|
||||
except IndexError:
|
||||
break
|
||||
self.previous = self.index
|
||||
result = self.buffer[self.index:self.index + pos]
|
||||
self.index += pos
|
||||
return result
|
||||
|
||||
def get_quoted_word(self):
|
||||
current = self.current
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
close_quote = _quotes.get(current)
|
||||
is_quoted = bool(close_quote)
|
||||
if is_quoted:
|
||||
result = []
|
||||
_escaped_quotes = (current, close_quote)
|
||||
else:
|
||||
result = [current]
|
||||
_escaped_quotes = _all_quotes
|
||||
|
||||
while not self.eof:
|
||||
current = self.get()
|
||||
if not current:
|
||||
if is_quoted:
|
||||
# unexpected EOF
|
||||
raise ExpectedClosingQuoteError(close_quote)
|
||||
return ''.join(result)
|
||||
|
||||
# currently we accept strings in the format of "hello world"
|
||||
# to embed a quote inside the string you must escape it: "a \"world\""
|
||||
if current == '\\':
|
||||
next_char = self.get()
|
||||
if not next_char:
|
||||
# string ends with \ and no character after it
|
||||
if is_quoted:
|
||||
# if we're quoted then we're expecting a closing quote
|
||||
raise ExpectedClosingQuoteError(close_quote)
|
||||
# if we aren't then we just let it through
|
||||
return ''.join(result)
|
||||
|
||||
if next_char in _escaped_quotes:
|
||||
# escaped quote
|
||||
result.append(next_char)
|
||||
else:
|
||||
# different escape character, ignore it
|
||||
self.undo()
|
||||
result.append(current)
|
||||
continue
|
||||
|
||||
if not is_quoted and current in _all_quotes:
|
||||
# we aren't quoted
|
||||
raise UnexpectedQuoteError(current)
|
||||
|
||||
# closing quote
|
||||
if is_quoted and current == close_quote:
|
||||
next_char = self.get()
|
||||
valid_eof = not next_char or next_char.isspace()
|
||||
if not valid_eof:
|
||||
raise InvalidEndOfQuotedStringError(next_char)
|
||||
|
||||
# we're quoted so it's okay
|
||||
return ''.join(result)
|
||||
|
||||
if current.isspace() and not is_quoted:
|
||||
# end of word found
|
||||
return ''.join(result)
|
||||
|
||||
result.append(current)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.format(self)
|
@@ -1,507 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import aiohttp
|
||||
import discord
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from discord.backoff import ExponentialBackoff
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Loop:
|
||||
"""A background task helper that abstracts the loop and reconnection logic for you.
|
||||
|
||||
The main interface to create this is through :func:`loop`.
|
||||
"""
|
||||
def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
|
||||
self.coro = coro
|
||||
self.reconnect = reconnect
|
||||
self.loop = loop
|
||||
self.count = count
|
||||
self._current_loop = 0
|
||||
self._task = None
|
||||
self._injected = None
|
||||
self._valid_exception = (
|
||||
OSError,
|
||||
discord.GatewayNotFound,
|
||||
discord.ConnectionClosed,
|
||||
aiohttp.ClientError,
|
||||
asyncio.TimeoutError,
|
||||
)
|
||||
|
||||
self._before_loop = None
|
||||
self._after_loop = None
|
||||
self._is_being_cancelled = False
|
||||
self._has_failed = False
|
||||
self._stop_next_iteration = False
|
||||
|
||||
if self.count is not None and self.count <= 0:
|
||||
raise ValueError('count must be greater than 0 or None.')
|
||||
|
||||
self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
|
||||
self._last_iteration_failed = False
|
||||
self._last_iteration = None
|
||||
self._next_iteration = None
|
||||
|
||||
if not inspect.iscoroutinefunction(self.coro):
|
||||
raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))
|
||||
|
||||
async def _call_loop_function(self, name, *args, **kwargs):
|
||||
coro = getattr(self, '_' + name)
|
||||
if coro is None:
|
||||
return
|
||||
|
||||
if self._injected is not None:
|
||||
await coro(self._injected, *args, **kwargs)
|
||||
else:
|
||||
await coro(*args, **kwargs)
|
||||
|
||||
async def _loop(self, *args, **kwargs):
|
||||
backoff = ExponentialBackoff()
|
||||
await self._call_loop_function('before_loop')
|
||||
sleep_until = discord.utils.sleep_until
|
||||
self._last_iteration_failed = False
|
||||
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
|
||||
try:
|
||||
await asyncio.sleep(0) # allows canceling in before_loop
|
||||
while True:
|
||||
if not self._last_iteration_failed:
|
||||
self._last_iteration = self._next_iteration
|
||||
self._next_iteration = self._get_next_sleep_time()
|
||||
try:
|
||||
await self.coro(*args, **kwargs)
|
||||
self._last_iteration_failed = False
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if now > self._next_iteration:
|
||||
self._next_iteration = now
|
||||
except self._valid_exception:
|
||||
self._last_iteration_failed = True
|
||||
if not self.reconnect:
|
||||
raise
|
||||
await asyncio.sleep(backoff.delay())
|
||||
else:
|
||||
await sleep_until(self._next_iteration)
|
||||
|
||||
if self._stop_next_iteration:
|
||||
return
|
||||
self._current_loop += 1
|
||||
if self._current_loop == self.count:
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
self._is_being_cancelled = True
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._has_failed = True
|
||||
await self._call_loop_function('error', exc)
|
||||
raise exc
|
||||
finally:
|
||||
await self._call_loop_function('after_loop')
|
||||
self._is_being_cancelled = False
|
||||
self._current_loop = 0
|
||||
self._stop_next_iteration = False
|
||||
self._has_failed = False
|
||||
|
||||
def __get__(self, obj, objtype):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
|
||||
count=self.count, reconnect=self.reconnect, loop=self.loop)
|
||||
copy._injected = obj
|
||||
copy._before_loop = self._before_loop
|
||||
copy._after_loop = self._after_loop
|
||||
copy._error = self._error
|
||||
setattr(obj, self.coro.__name__, copy)
|
||||
return copy
|
||||
|
||||
@property
|
||||
def current_loop(self):
|
||||
""":class:`int`: The current iteration of the loop."""
|
||||
return self._current_loop
|
||||
|
||||
@property
|
||||
def next_iteration(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._task is None:
|
||||
return None
|
||||
elif self._task and self._task.done() or self._stop_next_iteration:
|
||||
return None
|
||||
return self._next_iteration
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
r"""|coro|
|
||||
|
||||
Calls the internal callback that the task holds.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*args
|
||||
The arguments to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
"""
|
||||
|
||||
if self._injected is not None:
|
||||
args = (self._injected, *args)
|
||||
|
||||
return await self.coro(*args, **kwargs)
|
||||
|
||||
def start(self, *args, **kwargs):
|
||||
r"""Starts the internal task in the event loop.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*args
|
||||
The arguments to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
|
||||
Raises
|
||||
--------
|
||||
RuntimeError
|
||||
A task has already been launched and is running.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`asyncio.Task`
|
||||
The task that has been created.
|
||||
"""
|
||||
|
||||
if self._task is not None and not self._task.done():
|
||||
raise RuntimeError('Task is already launched and is not completed.')
|
||||
|
||||
if self._injected is not None:
|
||||
args = (self._injected, *args)
|
||||
|
||||
if self.loop is None:
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
self._task = self.loop.create_task(self._loop(*args, **kwargs))
|
||||
return self._task
|
||||
|
||||
def stop(self):
|
||||
r"""Gracefully stops the task from running.
|
||||
|
||||
Unlike :meth:`cancel`\, this allows the task to finish its
|
||||
current iteration before gracefully exiting.
|
||||
|
||||
.. note::
|
||||
|
||||
If the internal function raises an error that can be
|
||||
handled before finishing then it will retry until
|
||||
it succeeds.
|
||||
|
||||
If this is undesirable, either remove the error handling
|
||||
before stopping via :meth:`clear_exception_types` or
|
||||
use :meth:`cancel` instead.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
if self._task and not self._task.done():
|
||||
self._stop_next_iteration = True
|
||||
|
||||
def _can_be_cancelled(self):
|
||||
return not self._is_being_cancelled and self._task and not self._task.done()
|
||||
|
||||
def cancel(self):
|
||||
"""Cancels the internal task, if it is running."""
|
||||
if self._can_be_cancelled():
|
||||
self._task.cancel()
|
||||
|
||||
def restart(self, *args, **kwargs):
|
||||
r"""A convenience method to restart the internal task.
|
||||
|
||||
.. note::
|
||||
|
||||
Due to the way this function works, the task is not
|
||||
returned like :meth:`start`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*args
|
||||
The arguments to to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
"""
|
||||
|
||||
def restart_when_over(fut, *, args=args, kwargs=kwargs):
|
||||
self._task.remove_done_callback(restart_when_over)
|
||||
self.start(*args, **kwargs)
|
||||
|
||||
if self._can_be_cancelled():
|
||||
self._task.add_done_callback(restart_when_over)
|
||||
self._task.cancel()
|
||||
|
||||
def add_exception_type(self, *exceptions):
|
||||
r"""Adds exception types to be handled during the reconnect logic.
|
||||
|
||||
By default the exception types handled are those handled by
|
||||
:meth:`discord.Client.connect`\, which includes a lot of internet disconnection
|
||||
errors.
|
||||
|
||||
This function is useful if you're interacting with a 3rd party library that
|
||||
raises its own set of exceptions.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*exceptions: Type[:class:`BaseException`]
|
||||
An argument list of exception classes to handle.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
An exception passed is either not a class or not inherited from :class:`BaseException`.
|
||||
"""
|
||||
|
||||
for exc in exceptions:
|
||||
if not inspect.isclass(exc):
|
||||
raise TypeError('{0!r} must be a class.'.format(exc))
|
||||
if not issubclass(exc, BaseException):
|
||||
raise TypeError('{0!r} must inherit from BaseException.'.format(exc))
|
||||
|
||||
self._valid_exception = (*self._valid_exception, *exceptions)
|
||||
|
||||
def clear_exception_types(self):
|
||||
"""Removes all exception types that are handled.
|
||||
|
||||
.. note::
|
||||
|
||||
This operation obviously cannot be undone!
|
||||
"""
|
||||
self._valid_exception = tuple()
|
||||
|
||||
def remove_exception_type(self, *exceptions):
|
||||
r"""Removes exception types from being handled during the reconnect logic.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*exceptions: Type[:class:`BaseException`]
|
||||
An argument list of exception classes to handle.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`bool`
|
||||
Whether all exceptions were successfully removed.
|
||||
"""
|
||||
old_length = len(self._valid_exception)
|
||||
self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
|
||||
return len(self._valid_exception) == old_length - len(exceptions)
|
||||
|
||||
def get_task(self):
|
||||
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
|
||||
return self._task
|
||||
|
||||
def is_being_cancelled(self):
|
||||
"""Whether the task is being cancelled."""
|
||||
return self._is_being_cancelled
|
||||
|
||||
def failed(self):
|
||||
""":class:`bool`: Whether the internal task has failed.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return self._has_failed
|
||||
|
||||
def is_running(self):
|
||||
""":class:`bool`: Check if the task is currently running.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
return not bool(self._task.done()) if self._task else False
|
||||
|
||||
async def _error(self, *args):
|
||||
exception = args[-1]
|
||||
print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
def before_loop(self, coro):
|
||||
"""A decorator that registers a coroutine to be called before the loop starts running.
|
||||
|
||||
This is useful if you want to wait for some bot state before the loop starts,
|
||||
such as :meth:`discord.Client.wait_until_ready`.
|
||||
|
||||
The coroutine must take no arguments (except ``self`` in a class context).
|
||||
|
||||
Parameters
|
||||
------------
|
||||
coro: :ref:`coroutine <coroutine>`
|
||||
The coroutine to register before the loop runs.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The function was not a coroutine.
|
||||
"""
|
||||
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
|
||||
self._before_loop = coro
|
||||
return coro
|
||||
|
||||
def after_loop(self, coro):
|
||||
"""A decorator that register a coroutine to be called after the loop finished running.
|
||||
|
||||
The coroutine must take no arguments (except ``self`` in a class context).
|
||||
|
||||
.. note::
|
||||
|
||||
This coroutine is called even during cancellation. If it is desirable
|
||||
to tell apart whether something was cancelled or not, check to see
|
||||
whether :meth:`is_being_cancelled` is ``True`` or not.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
coro: :ref:`coroutine <coroutine>`
|
||||
The coroutine to register after the loop finishes.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The function was not a coroutine.
|
||||
"""
|
||||
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
|
||||
self._after_loop = coro
|
||||
return coro
|
||||
|
||||
def error(self, coro):
|
||||
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
||||
|
||||
The coroutine must take only one argument the exception raised (except ``self`` in a class context).
|
||||
|
||||
By default this prints to :data:`sys.stderr` however it could be
|
||||
overridden to have a different implementation.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Parameters
|
||||
------------
|
||||
coro: :ref:`coroutine <coroutine>`
|
||||
The coroutine to register in the event of an unhandled exception.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The function was not a coroutine.
|
||||
"""
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
|
||||
self._error = coro
|
||||
return coro
|
||||
|
||||
def _get_next_sleep_time(self):
|
||||
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
|
||||
|
||||
def change_interval(self, *, seconds=0, minutes=0, hours=0):
|
||||
"""Changes the interval for the sleep time.
|
||||
|
||||
.. note::
|
||||
|
||||
This only applies on the next loop iteration. If it is desirable for the change of interval
|
||||
to be applied right away, cancel the task with :meth:`cancel`.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
Parameters
|
||||
------------
|
||||
seconds: :class:`float`
|
||||
The number of seconds between every iteration.
|
||||
minutes: :class:`float`
|
||||
The number of minutes between every iteration.
|
||||
hours: :class:`float`
|
||||
The number of hours between every iteration.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
An invalid value was given.
|
||||
"""
|
||||
|
||||
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
|
||||
if sleep < 0:
|
||||
raise ValueError('Total number of seconds cannot be less than zero.')
|
||||
|
||||
self._sleep = sleep
|
||||
self.seconds = seconds
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
|
||||
def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
|
||||
"""A decorator that schedules a task in the background for you with
|
||||
optional reconnect logic. The decorator returns a :class:`Loop`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
seconds: :class:`float`
|
||||
The number of seconds between every iteration.
|
||||
minutes: :class:`float`
|
||||
The number of minutes between every iteration.
|
||||
hours: :class:`float`
|
||||
The number of hours between every iteration.
|
||||
count: Optional[:class:`int`]
|
||||
The number of loops to do, ``None`` if it should be an
|
||||
infinite loop.
|
||||
reconnect: :class:`bool`
|
||||
Whether to handle errors and restart the task
|
||||
using an exponential back-off algorithm similar to the
|
||||
one used in :meth:`discord.Client.connect`.
|
||||
loop: :class:`asyncio.AbstractEventLoop`
|
||||
The loop to use to register the task, if not given
|
||||
defaults to :func:`asyncio.get_event_loop`.
|
||||
|
||||
Raises
|
||||
--------
|
||||
ValueError
|
||||
An invalid value was given.
|
||||
TypeError
|
||||
The function was not a coroutine.
|
||||
"""
|
||||
def decorator(func):
|
||||
kwargs = {
|
||||
'seconds': seconds,
|
||||
'minutes': minutes,
|
||||
'hours': hours,
|
||||
'count': count,
|
||||
'reconnect': reconnect,
|
||||
'loop': loop
|
||||
}
|
||||
return Loop(func, **kwargs)
|
||||
return decorator
|
Binary file not shown.
@@ -1,112 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import io
|
||||
|
||||
class File:
|
||||
r"""A parameter object used for :meth:`abc.Messageable.send`
|
||||
for sending file objects.
|
||||
|
||||
.. note::
|
||||
|
||||
File objects are single use and are not meant to be reused in
|
||||
multiple :meth:`abc.Messageable.send`\s.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
fp: Union[:class:`str`, :class:`io.BufferedIOBase`]
|
||||
A file-like object opened in binary mode and read mode
|
||||
or a filename representing a file in the hard drive to
|
||||
open.
|
||||
|
||||
.. note::
|
||||
|
||||
If the file-like object passed is opened via ``open`` then the
|
||||
modes 'rb' should be used.
|
||||
|
||||
To pass binary data, consider usage of ``io.BytesIO``.
|
||||
|
||||
filename: Optional[:class:`str`]
|
||||
The filename to display when uploading to Discord.
|
||||
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
|
||||
a string then the ``filename`` will default to the string given.
|
||||
spoiler: :class:`bool`
|
||||
Whether the attachment is a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
|
||||
|
||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
||||
self.fp = fp
|
||||
|
||||
if isinstance(fp, io.IOBase):
|
||||
if not (fp.seekable() and fp.readable()):
|
||||
raise ValueError('File buffer {!r} must be seekable and readable'.format(fp))
|
||||
self.fp = fp
|
||||
self._original_pos = fp.tell()
|
||||
self._owner = False
|
||||
else:
|
||||
self.fp = open(fp, 'rb')
|
||||
self._original_pos = 0
|
||||
self._owner = True
|
||||
|
||||
# aiohttp only uses two methods from IOBase
|
||||
# read and close, since I want to control when the files
|
||||
# close, I need to stub it so it doesn't close unless
|
||||
# I tell it to
|
||||
self._closer = self.fp.close
|
||||
self.fp.close = lambda: None
|
||||
|
||||
if filename is None:
|
||||
if isinstance(fp, str):
|
||||
_, self.filename = os.path.split(fp)
|
||||
else:
|
||||
self.filename = getattr(fp, 'name', None)
|
||||
else:
|
||||
self.filename = filename
|
||||
|
||||
if spoiler and self.filename is not None and not self.filename.startswith('SPOILER_'):
|
||||
self.filename = 'SPOILER_' + self.filename
|
||||
|
||||
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
|
||||
|
||||
def reset(self, *, seek=True):
|
||||
# The `seek` parameter is needed because
|
||||
# the retry-loop is iterated over multiple times
|
||||
# starting from 0, as an implementation quirk
|
||||
# the resetting must be done at the beginning
|
||||
# before a request is done, since the first index
|
||||
# is 0, and thus false, then this prevents an
|
||||
# unnecessary seek since it's the first request
|
||||
# done.
|
||||
if seek:
|
||||
self.fp.seek(self._original_pos)
|
||||
|
||||
def close(self):
|
||||
self.fp.close = self._closer
|
||||
if self._owner:
|
||||
self._closer()
|
@@ -1,940 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 .enums import UserFlags
|
||||
|
||||
__all__ = (
|
||||
'SystemChannelFlags',
|
||||
'MessageFlags',
|
||||
'PublicUserFlags',
|
||||
'Intents',
|
||||
'MemberCacheFlags',
|
||||
)
|
||||
|
||||
class flag_value:
|
||||
def __init__(self, func):
|
||||
self.flag = func(None)
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
return instance._has_flag(self.flag)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance._set_flag(self.flag, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<flag_value flag={.flag!r}>'.format(self)
|
||||
|
||||
class alias_flag_value(flag_value):
|
||||
pass
|
||||
|
||||
def fill_with_flags(*, inverted=False):
|
||||
def decorator(cls):
|
||||
cls.VALID_FLAGS = {
|
||||
name: value.flag
|
||||
for name, value in cls.__dict__.items()
|
||||
if isinstance(value, flag_value)
|
||||
}
|
||||
|
||||
if inverted:
|
||||
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
cls.DEFAULT_VALUE = -1 + (2 ** max_bits)
|
||||
else:
|
||||
cls.DEFAULT_VALUE = 0
|
||||
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
# n.b. flags must inherit from this and use the decorator above
|
||||
class BaseFlags:
|
||||
__slots__ = ('value',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls, value):
|
||||
self = cls.__new__(cls)
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s value=%s>' % (self.__class__.__name__, self.value)
|
||||
|
||||
def __iter__(self):
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, alias_flag_value):
|
||||
continue
|
||||
|
||||
if isinstance(value, flag_value):
|
||||
yield (name, self._has_flag(value.flag))
|
||||
|
||||
def _has_flag(self, o):
|
||||
return (self.value & o) == o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
if toggle is True:
|
||||
self.value |= o
|
||||
elif toggle is False:
|
||||
self.value &= ~o
|
||||
else:
|
||||
raise TypeError('Value to set for %s must be a bool.' % self.__class__.__name__)
|
||||
|
||||
@fill_with_flags(inverted=True)
|
||||
class SystemChannelFlags(BaseFlags):
|
||||
r"""Wraps up a Discord system channel flag value.
|
||||
|
||||
Similar to :class:`Permissions`\, the properties provided are two way.
|
||||
You can set and retrieve individual bits using the properties as if they
|
||||
were regular bools. This allows you to edit the system flags easily.
|
||||
|
||||
To construct an object you can pass keyword arguments denoting the flags
|
||||
to enable or disable.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two flags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two flags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. 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.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. This value is a bit array field of a 53-bit integer
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
# For some reason the flags for system channels are "inverted"
|
||||
# ergo, if they're set then it means "suppress" (off in the GUI toggle)
|
||||
# Since this is counter-intuitive from an API perspective and annoying
|
||||
# these will be inverted automatically
|
||||
|
||||
def _has_flag(self, o):
|
||||
return (self.value & o) != o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
if toggle is True:
|
||||
self.value &= ~o
|
||||
elif toggle is False:
|
||||
self.value |= o
|
||||
else:
|
||||
raise TypeError('Value to set for SystemChannelFlags must be a bool.')
|
||||
|
||||
@flag_value
|
||||
def join_notifications(self):
|
||||
""":class:`bool`: Returns ``True`` if the system channel is used for member join notifications."""
|
||||
return 1
|
||||
|
||||
@flag_value
|
||||
def premium_subscriptions(self):
|
||||
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
|
||||
return 2
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class MessageFlags(BaseFlags):
|
||||
r"""Wraps up a Discord Message flag value.
|
||||
|
||||
See :class:`SystemChannelFlags`.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two flags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two flags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. 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.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. This value is a bit array field of a 53-bit integer
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@flag_value
|
||||
def crossposted(self):
|
||||
""":class:`bool`: Returns ``True`` if the message is the original crossposted message."""
|
||||
return 1
|
||||
|
||||
@flag_value
|
||||
def is_crossposted(self):
|
||||
""":class:`bool`: Returns ``True`` if the message was crossposted from another channel."""
|
||||
return 2
|
||||
|
||||
@flag_value
|
||||
def suppress_embeds(self):
|
||||
""":class:`bool`: Returns ``True`` if the message's embeds have been suppressed."""
|
||||
return 4
|
||||
|
||||
@flag_value
|
||||
def source_message_deleted(self):
|
||||
""":class:`bool`: Returns ``True`` if the source message for this crosspost has been deleted."""
|
||||
return 8
|
||||
|
||||
@flag_value
|
||||
def urgent(self):
|
||||
""":class:`bool`: Returns ``True`` if the source message is an urgent message.
|
||||
|
||||
An urgent message is one sent by Discord Trust and Safety.
|
||||
"""
|
||||
return 16
|
||||
|
||||
@fill_with_flags()
|
||||
class PublicUserFlags(BaseFlags):
|
||||
r"""Wraps up the Discord User Public flags.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two PublicUserFlags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two PublicUserFlags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. 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.
|
||||
Note that aliases are not shown.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. This value is a bit array field of a 53-bit integer
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@flag_value
|
||||
def staff(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Discord Employee."""
|
||||
return UserFlags.staff.value
|
||||
|
||||
@flag_value
|
||||
def partner(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Discord Partner."""
|
||||
return UserFlags.partner.value
|
||||
|
||||
@flag_value
|
||||
def hypesquad(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a HypeSquad Events member."""
|
||||
return UserFlags.hypesquad.value
|
||||
|
||||
@flag_value
|
||||
def bug_hunter(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Bug Hunter"""
|
||||
return UserFlags.bug_hunter.value
|
||||
|
||||
@flag_value
|
||||
def hypesquad_bravery(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a HypeSquad Bravery member."""
|
||||
return UserFlags.hypesquad_bravery.value
|
||||
|
||||
@flag_value
|
||||
def hypesquad_brilliance(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a HypeSquad Brilliance member."""
|
||||
return UserFlags.hypesquad_brilliance.value
|
||||
|
||||
@flag_value
|
||||
def hypesquad_balance(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a HypeSquad Balance member."""
|
||||
return UserFlags.hypesquad_balance.value
|
||||
|
||||
@flag_value
|
||||
def early_supporter(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is an Early Supporter."""
|
||||
return UserFlags.early_supporter.value
|
||||
|
||||
@flag_value
|
||||
def team_user(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Team User."""
|
||||
return UserFlags.team_user.value
|
||||
|
||||
@flag_value
|
||||
def system(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a system user (i.e. represents Discord officially)."""
|
||||
return UserFlags.system.value
|
||||
|
||||
@flag_value
|
||||
def bug_hunter_level_2(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Bug Hunter Level 2"""
|
||||
return UserFlags.bug_hunter_level_2.value
|
||||
|
||||
@flag_value
|
||||
def verified_bot(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Verified Bot."""
|
||||
return UserFlags.verified_bot.value
|
||||
|
||||
@flag_value
|
||||
def verified_bot_developer(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is an Early Verified Bot Developer."""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
@alias_flag_value
|
||||
def early_verified_bot_developer(self):
|
||||
""":class:`bool`: An alias for :attr:`verified_bot_developer`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
def all(self):
|
||||
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
|
||||
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class Intents(BaseFlags):
|
||||
r"""Wraps up a Discord gateway intent flag.
|
||||
|
||||
Similar to :class:`Permissions`\, the properties provided are two way.
|
||||
You can set and retrieve individual bits using the properties as if they
|
||||
were regular bools.
|
||||
|
||||
To construct an object you can pass keyword arguments denoting the flags
|
||||
to enable or disable.
|
||||
|
||||
This is used to disable certain gateway features that are unnecessary to
|
||||
run your bot. To make use of this, it is passed to the ``intents`` keyword
|
||||
argument of :class:`Client`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two flags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two flags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. 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.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. You should query flags via the properties
|
||||
rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
self = cls.__new__(cls)
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
"""A factory method that creates a :class:`Intents` with everything disabled."""
|
||||
self = cls.__new__(cls)
|
||||
self.value = self.DEFAULT_VALUE
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||
except :attr:`presences` and :attr:`members`.
|
||||
"""
|
||||
self = cls.all()
|
||||
self.presences = False
|
||||
self.members = False
|
||||
return self
|
||||
|
||||
@flag_value
|
||||
def guilds(self):
|
||||
""":class:`bool`: Whether guild related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_join`
|
||||
- :func:`on_guild_remove`
|
||||
- :func:`on_guild_available`
|
||||
- :func:`on_guild_unavailable`
|
||||
- :func:`on_guild_channel_update`
|
||||
- :func:`on_guild_channel_create`
|
||||
- :func:`on_guild_channel_delete`
|
||||
- :func:`on_guild_channel_pins_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Client.guilds`
|
||||
- :class:`Guild` and all its attributes.
|
||||
- :meth:`Client.get_channel`
|
||||
- :meth:`Client.get_all_channels`
|
||||
|
||||
It is highly advisable to leave this intent enabled for your bot to function.
|
||||
"""
|
||||
return 1 << 0
|
||||
|
||||
@flag_value
|
||||
def members(self):
|
||||
""":class:`bool`: Whether guild member related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_member_join`
|
||||
- :func:`on_member_remove`
|
||||
- :func:`on_member_update` (nickname, roles)
|
||||
- :func:`on_user_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :meth:`Client.get_all_members`
|
||||
- :meth:`Guild.chunk`
|
||||
- :meth:`Guild.fetch_members`
|
||||
- :meth:`Guild.get_member`
|
||||
- :attr:`Guild.members`
|
||||
- :attr:`Member.roles`
|
||||
- :attr:`Member.nick`
|
||||
- :attr:`Member.premium_since`
|
||||
- :attr:`User.name`
|
||||
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :attr:`User.discriminator`
|
||||
|
||||
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, this requires opting in explicitly via the developer portal as well.
|
||||
Bots in over 100 guilds will need to apply to Discord for verification.
|
||||
"""
|
||||
return 1 << 1
|
||||
|
||||
@flag_value
|
||||
def bans(self):
|
||||
""":class:`bool`: Whether guild ban related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_member_ban`
|
||||
- :func:`on_member_unban`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 2
|
||||
|
||||
@flag_value
|
||||
def emojis(self):
|
||||
""":class:`bool`: Whether guild emoji related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_emojis_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Emoji`
|
||||
- :meth:`Client.get_emoji`
|
||||
- :meth:`Client.emojis`
|
||||
- :attr:`Guild.emojis`
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@flag_value
|
||||
def integrations(self):
|
||||
""":class:`bool`: Whether guild integration related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_integrations_update`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 4
|
||||
|
||||
@flag_value
|
||||
def webhooks(self):
|
||||
""":class:`bool`: Whether guild webhook related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_webhooks_update`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 5
|
||||
|
||||
@flag_value
|
||||
def invites(self):
|
||||
""":class:`bool`: Whether guild invite related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_invite_create`
|
||||
- :func:`on_invite_delete`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 6
|
||||
|
||||
@flag_value
|
||||
def voice_states(self):
|
||||
""":class:`bool`: Whether guild voice state related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_voice_state_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`VoiceChannel.members`
|
||||
- :attr:`VoiceChannel.voice_states`
|
||||
- :attr:`Member.voice`
|
||||
"""
|
||||
return 1 << 7
|
||||
|
||||
@flag_value
|
||||
def presences(self):
|
||||
""":class:`bool`: Whether guild presence related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_member_update` (activities, status)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Member.activities`
|
||||
- :attr:`Member.status`
|
||||
- :attr:`Member.raw_status`
|
||||
|
||||
For more information go to the :ref:`presence intent documentation <need_presence_intent>`.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, this requires opting in explicitly via the developer portal as well.
|
||||
Bots in over 100 guilds will need to apply to Discord for verification.
|
||||
"""
|
||||
return 1 << 8
|
||||
|
||||
@alias_flag_value
|
||||
def messages(self):
|
||||
""":class:`bool`: Whether guild and direct message related events are enabled.
|
||||
|
||||
This is a shortcut to set or get both :attr:`guild_messages` and :attr:`dm_messages`.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (both guilds and DMs)
|
||||
- :func:`on_message_edit` (both guilds and DMs)
|
||||
- :func:`on_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_edit` (both guilds and DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages`
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (both guilds and DMs)
|
||||
- :func:`on_reaction_remove` (both guilds and DMs)
|
||||
- :func:`on_reaction_clear` (both guilds and DMs)
|
||||
"""
|
||||
return (1 << 9) | (1 << 12)
|
||||
|
||||
@flag_value
|
||||
def guild_messages(self):
|
||||
""":class:`bool`: Whether guild message related events are enabled.
|
||||
|
||||
See also :attr:`dm_messages` for DMs or :attr:`messages` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (only for guilds)
|
||||
- :func:`on_message_edit` (only for guilds)
|
||||
- :func:`on_message_delete` (only for guilds)
|
||||
- :func:`on_raw_message_delete` (only for guilds)
|
||||
- :func:`on_raw_message_edit` (only for guilds)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages` (only for guilds)
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for guilds)
|
||||
- :func:`on_reaction_remove` (only for guilds)
|
||||
- :func:`on_reaction_clear` (only for guilds)
|
||||
"""
|
||||
return 1 << 9
|
||||
|
||||
@flag_value
|
||||
def dm_messages(self):
|
||||
""":class:`bool`: Whether direct message related events are enabled.
|
||||
|
||||
See also :attr:`guild_messages` for guilds or :attr:`messages` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (only for DMs)
|
||||
- :func:`on_message_edit` (only for DMs)
|
||||
- :func:`on_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_edit` (only for DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages` (only for DMs)
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for DMs)
|
||||
- :func:`on_reaction_remove` (only for DMs)
|
||||
- :func:`on_reaction_clear` (only for DMs)
|
||||
"""
|
||||
return 1 << 12
|
||||
|
||||
@alias_flag_value
|
||||
def reactions(self):
|
||||
""":class:`bool`: Whether guild and direct message reaction related events are enabled.
|
||||
|
||||
This is a shortcut to set or get both :attr:`guild_reactions` and :attr:`dm_reactions`.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (both guilds and DMs)
|
||||
- :func:`on_reaction_remove` (both guilds and DMs)
|
||||
- :func:`on_reaction_clear` (both guilds and DMs)
|
||||
- :func:`on_raw_reaction_add` (both guilds and DMs)
|
||||
- :func:`on_raw_reaction_remove` (both guilds and DMs)
|
||||
- :func:`on_raw_reaction_clear` (both guilds and DMs)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (both guild and DM messages)
|
||||
"""
|
||||
return (1 << 10) | (1 << 13)
|
||||
|
||||
@flag_value
|
||||
def guild_reactions(self):
|
||||
""":class:`bool`: Whether guild message reaction related events are enabled.
|
||||
|
||||
See also :attr:`dm_reactions` for DMs or :attr:`reactions` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for guilds)
|
||||
- :func:`on_reaction_remove` (only for guilds)
|
||||
- :func:`on_reaction_clear` (only for guilds)
|
||||
- :func:`on_raw_reaction_add` (only for guilds)
|
||||
- :func:`on_raw_reaction_remove` (only for guilds)
|
||||
- :func:`on_raw_reaction_clear` (only for guilds)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (only for guild messages)
|
||||
"""
|
||||
return 1 << 10
|
||||
|
||||
@flag_value
|
||||
def dm_reactions(self):
|
||||
""":class:`bool`: Whether direct message reaction related events are enabled.
|
||||
|
||||
See also :attr:`guild_reactions` for guilds or :attr:`reactions` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for DMs)
|
||||
- :func:`on_reaction_remove` (only for DMs)
|
||||
- :func:`on_reaction_clear` (only for DMs)
|
||||
- :func:`on_raw_reaction_add` (only for DMs)
|
||||
- :func:`on_raw_reaction_remove` (only for DMs)
|
||||
- :func:`on_raw_reaction_clear` (only for DMs)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (only for DM messages)
|
||||
"""
|
||||
return 1 << 13
|
||||
|
||||
@alias_flag_value
|
||||
def typing(self):
|
||||
""":class:`bool`: Whether guild and direct message typing related events are enabled.
|
||||
|
||||
This is a shortcut to set or get both :attr:`guild_typing` and :attr:`dm_typing`.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (both guilds and DMs)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return (1 << 11) | (1 << 14)
|
||||
|
||||
@flag_value
|
||||
def guild_typing(self):
|
||||
""":class:`bool`: Whether guild and direct message typing related events are enabled.
|
||||
|
||||
See also :attr:`dm_typing` for DMs or :attr:`typing` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (only for guilds)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 11
|
||||
|
||||
@flag_value
|
||||
def dm_typing(self):
|
||||
""":class:`bool`: Whether guild and direct message typing related events are enabled.
|
||||
|
||||
See also :attr:`guild_typing` for guilds or :attr:`typing` for both.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (only for DMs)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 14
|
||||
|
||||
@fill_with_flags()
|
||||
class MemberCacheFlags(BaseFlags):
|
||||
"""Controls the library's cache policy when it comes to members.
|
||||
|
||||
This allows for finer grained control over what members are cached.
|
||||
Note that the bot's own member is always cached. This class is passed
|
||||
to the ``member_cache_flags`` parameter in :class:`Client`.
|
||||
|
||||
Due to a quirk in how Discord works, in order to ensure proper cleanup
|
||||
of cache resources it is recommended to have :attr:`Intents.members`
|
||||
enabled. Otherwise the library cannot know when a member leaves a guild and
|
||||
is thus unable to cleanup after itself.
|
||||
|
||||
To construct an object you can pass keyword arguments denoting the flags
|
||||
to enable or disable.
|
||||
|
||||
The default value is all flags enabled.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two flags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two flags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. 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.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. You should query flags via the properties
|
||||
rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
bits = max(self.VALID_FLAGS.values()).bit_length()
|
||||
self.value = (1 << bits) - 1
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
self = cls.__new__(cls)
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything disabled."""
|
||||
self = cls.__new__(cls)
|
||||
self.value = self.DEFAULT_VALUE
|
||||
return self
|
||||
|
||||
@property
|
||||
def _empty(self):
|
||||
return self.value == self.DEFAULT_VALUE
|
||||
|
||||
@flag_value
|
||||
def online(self):
|
||||
""":class:`bool`: Whether to cache members with a status.
|
||||
|
||||
For example, members that are part of the initial ``GUILD_CREATE``
|
||||
or become online at a later point. This requires :attr:`Intents.presences`.
|
||||
|
||||
Members that go offline are no longer cached.
|
||||
"""
|
||||
return 1
|
||||
|
||||
@flag_value
|
||||
def voice(self):
|
||||
""":class:`bool`: Whether to cache members that are in voice.
|
||||
|
||||
This requires :attr:`Intents.voice_states`.
|
||||
|
||||
Members that leave voice are no longer cached.
|
||||
"""
|
||||
return 2
|
||||
|
||||
@flag_value
|
||||
def joined(self):
|
||||
""":class:`bool`: Whether to cache members that joined the guild
|
||||
or are chunked as part of the initial log in flow.
|
||||
|
||||
This requires :attr:`Intents.members`.
|
||||
|
||||
Members that leave the guild are no longer cached.
|
||||
"""
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def from_intents(cls, intents):
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` based on
|
||||
the currently selected :class:`Intents`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
intents: :class:`Intents`
|
||||
The intents to select from.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`MemberCacheFlags`
|
||||
The resulting member cache flags.
|
||||
"""
|
||||
|
||||
self = cls.none()
|
||||
if intents.members:
|
||||
self.joined = True
|
||||
if intents.presences:
|
||||
self.online = True
|
||||
if intents.voice_states:
|
||||
self.voice = True
|
||||
|
||||
if not self.joined and self.online and self.voice:
|
||||
self.voice = False
|
||||
|
||||
return self
|
||||
|
||||
def _verify_intents(self, intents):
|
||||
if self.online and not intents.presences:
|
||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
||||
|
||||
if self.voice and not intents.voice_states:
|
||||
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
|
||||
|
||||
if self.joined and not intents.members:
|
||||
raise ValueError('MemberCacheFlags.joined requires Intents.members')
|
||||
|
||||
if not self.joined and self.voice and self.online:
|
||||
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
|
||||
'to properly evict members from the cache.'
|
||||
raise ValueError(msg)
|
||||
|
||||
@property
|
||||
def _voice_only(self):
|
||||
return self.value == 2
|
||||
|
||||
@property
|
||||
def _online_only(self):
|
||||
return self.value == 1
|
@@ -1,902 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple, deque
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
import zlib
|
||||
|
||||
import aiohttp
|
||||
|
||||
from . import utils
|
||||
from .activity import BaseActivity
|
||||
from .enums import SpeakingState
|
||||
from .errors import ConnectionClosed, InvalidArgument
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = (
|
||||
'DiscordWebSocket',
|
||||
'KeepAliveHandler',
|
||||
'VoiceKeepAliveHandler',
|
||||
'DiscordVoiceWebSocket',
|
||||
'ReconnectWebSocket',
|
||||
)
|
||||
|
||||
class ReconnectWebSocket(Exception):
|
||||
"""Signals to safely reconnect the websocket."""
|
||||
def __init__(self, shard_id, *, resume=True):
|
||||
self.shard_id = shard_id
|
||||
self.resume = resume
|
||||
self.op = 'RESUME' if resume else 'IDENTIFY'
|
||||
|
||||
class WebSocketClosure(Exception):
|
||||
"""An exception to make up for the fact that aiohttp doesn't signal closure."""
|
||||
pass
|
||||
|
||||
EventListener = namedtuple('EventListener', 'predicate event result future')
|
||||
|
||||
class GatewayRatelimiter:
|
||||
def __init__(self, count=110, per=60.0):
|
||||
# The default is 110 to give room for at least 10 heartbeats per minute
|
||||
self.max = count
|
||||
self.remaining = count
|
||||
self.window = 0.0
|
||||
self.per = per
|
||||
self.lock = asyncio.Lock()
|
||||
self.shard_id = None
|
||||
|
||||
def is_ratelimited(self):
|
||||
current = time.time()
|
||||
if current > self.window + self.per:
|
||||
return False
|
||||
return self.remaining == 0
|
||||
|
||||
def get_delay(self):
|
||||
current = time.time()
|
||||
|
||||
if current > self.window + self.per:
|
||||
self.remaining = self.max
|
||||
|
||||
if self.remaining == self.max:
|
||||
self.window = current
|
||||
|
||||
if self.remaining == 0:
|
||||
return self.per - (current - self.window)
|
||||
|
||||
self.remaining -= 1
|
||||
if self.remaining == 0:
|
||||
self.window = current
|
||||
|
||||
return 0.0
|
||||
|
||||
async def block(self):
|
||||
async with self.lock:
|
||||
delta = self.get_delay()
|
||||
if delta:
|
||||
log.warning('WebSocket in shard ID %s is ratelimited, waiting %.2f seconds', self.shard_id, delta)
|
||||
await asyncio.sleep(delta)
|
||||
|
||||
|
||||
class KeepAliveHandler(threading.Thread):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ws = kwargs.pop('ws', None)
|
||||
interval = kwargs.pop('interval', None)
|
||||
shard_id = kwargs.pop('shard_id', None)
|
||||
threading.Thread.__init__(self, *args, **kwargs)
|
||||
self.ws = ws
|
||||
self._main_thread_id = ws.thread_id
|
||||
self.interval = interval
|
||||
self.daemon = True
|
||||
self.shard_id = shard_id
|
||||
self.msg = 'Keeping shard ID %s websocket alive with sequence %s.'
|
||||
self.block_msg = 'Shard ID %s heartbeat blocked for more than %s seconds.'
|
||||
self.behind_msg = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.'
|
||||
self._stop_ev = threading.Event()
|
||||
self._last_ack = time.perf_counter()
|
||||
self._last_send = time.perf_counter()
|
||||
self._last_recv = time.perf_counter()
|
||||
self.latency = float('inf')
|
||||
self.heartbeat_timeout = ws._max_heartbeat_timeout
|
||||
|
||||
def run(self):
|
||||
while not self._stop_ev.wait(self.interval):
|
||||
if self._last_recv + self.heartbeat_timeout < time.perf_counter():
|
||||
log.warning("Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id)
|
||||
coro = self.ws.close(4000)
|
||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
||||
|
||||
try:
|
||||
f.result()
|
||||
except Exception:
|
||||
log.exception('An error occurred while stopping the gateway. Ignoring.')
|
||||
finally:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
data = self.get_payload()
|
||||
log.debug(self.msg, self.shard_id, data['d'])
|
||||
coro = self.ws.send_heartbeat(data)
|
||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
||||
try:
|
||||
# block until sending is complete
|
||||
total = 0
|
||||
while True:
|
||||
try:
|
||||
f.result(10)
|
||||
break
|
||||
except concurrent.futures.TimeoutError:
|
||||
total += 10
|
||||
try:
|
||||
frame = sys._current_frames()[self._main_thread_id]
|
||||
except KeyError:
|
||||
msg = self.block_msg
|
||||
else:
|
||||
stack = traceback.format_stack(frame)
|
||||
msg = '%s\nLoop thread traceback (most recent call last):\n%s' % (self.block_msg, ''.join(stack))
|
||||
log.warning(msg, self.shard_id, total)
|
||||
|
||||
except Exception:
|
||||
self.stop()
|
||||
else:
|
||||
self._last_send = time.perf_counter()
|
||||
|
||||
def get_payload(self):
|
||||
return {
|
||||
'op': self.ws.HEARTBEAT,
|
||||
'd': self.ws.sequence
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
self._stop_ev.set()
|
||||
|
||||
def tick(self):
|
||||
self._last_recv = time.perf_counter()
|
||||
|
||||
def ack(self):
|
||||
ack_time = time.perf_counter()
|
||||
self._last_ack = ack_time
|
||||
self.latency = ack_time - self._last_send
|
||||
if self.latency > 10:
|
||||
log.warning(self.behind_msg, self.shard_id, self.latency)
|
||||
|
||||
class VoiceKeepAliveHandler(KeepAliveHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.recent_ack_latencies = deque(maxlen=20)
|
||||
self.msg = 'Keeping shard ID %s voice websocket alive with timestamp %s.'
|
||||
self.block_msg = 'Shard ID %s voice heartbeat blocked for more than %s seconds'
|
||||
self.behind_msg = 'High socket latency, shard ID %s heartbeat is %.1fs behind'
|
||||
|
||||
def get_payload(self):
|
||||
return {
|
||||
'op': self.ws.HEARTBEAT,
|
||||
'd': int(time.time() * 1000)
|
||||
}
|
||||
|
||||
def ack(self):
|
||||
ack_time = time.perf_counter()
|
||||
self._last_ack = ack_time
|
||||
self._last_recv = ack_time
|
||||
self.latency = ack_time - self._last_send
|
||||
self.recent_ack_latencies.append(self.latency)
|
||||
|
||||
class DiscordClientWebSocketResponse(aiohttp.ClientWebSocketResponse):
|
||||
async def close(self, *, code: int = 4000, message: bytes = b'') -> bool:
|
||||
return await super().close(code=code, message=message)
|
||||
|
||||
class DiscordWebSocket:
|
||||
"""Implements a WebSocket for Discord's gateway v6.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
DISPATCH
|
||||
Receive only. Denotes an event to be sent to Discord, such as READY.
|
||||
HEARTBEAT
|
||||
When received tells Discord to keep the connection alive.
|
||||
When sent asks if your connection is currently alive.
|
||||
IDENTIFY
|
||||
Send only. Starts a new session.
|
||||
PRESENCE
|
||||
Send only. Updates your presence.
|
||||
VOICE_STATE
|
||||
Send only. Starts a new connection to a voice guild.
|
||||
VOICE_PING
|
||||
Send only. Checks ping time to a voice guild, do not use.
|
||||
RESUME
|
||||
Send only. Resumes an existing connection.
|
||||
RECONNECT
|
||||
Receive only. Tells the client to reconnect to a new gateway.
|
||||
REQUEST_MEMBERS
|
||||
Send only. Asks for the full member list of a guild.
|
||||
INVALIDATE_SESSION
|
||||
Receive only. Tells the client to optionally invalidate the session
|
||||
and IDENTIFY again.
|
||||
HELLO
|
||||
Receive only. Tells the client the heartbeat interval.
|
||||
HEARTBEAT_ACK
|
||||
Receive only. Confirms receiving of a heartbeat. Not having it implies
|
||||
a connection issue.
|
||||
GUILD_SYNC
|
||||
Send only. Requests a guild sync.
|
||||
gateway
|
||||
The gateway we are currently connected to.
|
||||
token
|
||||
The authentication token for discord.
|
||||
"""
|
||||
|
||||
DISPATCH = 0
|
||||
HEARTBEAT = 1
|
||||
IDENTIFY = 2
|
||||
PRESENCE = 3
|
||||
VOICE_STATE = 4
|
||||
VOICE_PING = 5
|
||||
RESUME = 6
|
||||
RECONNECT = 7
|
||||
REQUEST_MEMBERS = 8
|
||||
INVALIDATE_SESSION = 9
|
||||
HELLO = 10
|
||||
HEARTBEAT_ACK = 11
|
||||
GUILD_SYNC = 12
|
||||
|
||||
def __init__(self, socket, *, loop):
|
||||
self.socket = socket
|
||||
self.loop = loop
|
||||
|
||||
# an empty dispatcher to prevent crashes
|
||||
self._dispatch = lambda *args: None
|
||||
# generic event listeners
|
||||
self._dispatch_listeners = []
|
||||
# the keep alive
|
||||
self._keep_alive = None
|
||||
self.thread_id = threading.get_ident()
|
||||
|
||||
# ws related stuff
|
||||
self.session_id = None
|
||||
self.sequence = None
|
||||
self._zlib = zlib.decompressobj()
|
||||
self._buffer = bytearray()
|
||||
self._close_code = None
|
||||
self._rate_limiter = GatewayRatelimiter()
|
||||
|
||||
@property
|
||||
def open(self):
|
||||
return not self.socket.closed
|
||||
|
||||
def is_ratelimited(self):
|
||||
return self._rate_limiter.is_ratelimited()
|
||||
|
||||
@classmethod
|
||||
async def from_client(cls, client, *, initial=False, gateway=None, shard_id=None, session=None, sequence=None, resume=False):
|
||||
"""Creates a main websocket for Discord from a :class:`Client`.
|
||||
|
||||
This is for internal use only.
|
||||
"""
|
||||
gateway = gateway or await client.http.get_gateway()
|
||||
socket = await client.http.ws_connect(gateway)
|
||||
ws = cls(socket, loop=client.loop)
|
||||
|
||||
# dynamically add attributes needed
|
||||
ws.token = client.http.token
|
||||
ws._connection = client._connection
|
||||
ws._discord_parsers = client._connection.parsers
|
||||
ws._dispatch = client.dispatch
|
||||
ws.gateway = gateway
|
||||
ws.call_hooks = client._connection.call_hooks
|
||||
ws._initial_identify = initial
|
||||
ws.shard_id = shard_id
|
||||
ws._rate_limiter.shard_id = shard_id
|
||||
ws.shard_count = client._connection.shard_count
|
||||
ws.session_id = session
|
||||
ws.sequence = sequence
|
||||
ws._max_heartbeat_timeout = client._connection.heartbeat_timeout
|
||||
|
||||
client._connection._update_references(ws)
|
||||
|
||||
log.debug('Created websocket connected to %s', gateway)
|
||||
|
||||
# poll event for OP Hello
|
||||
await ws.poll_event()
|
||||
|
||||
if not resume:
|
||||
await ws.identify()
|
||||
return ws
|
||||
|
||||
await ws.resume()
|
||||
return ws
|
||||
|
||||
def wait_for(self, event, predicate, result=None):
|
||||
"""Waits for a DISPATCH'd event that meets the predicate.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
event: :class:`str`
|
||||
The event name in all upper case to wait for.
|
||||
predicate
|
||||
A function that takes a data parameter to check for event
|
||||
properties. The data parameter is the 'd' key in the JSON message.
|
||||
result
|
||||
A function that takes the same data parameter and executes to send
|
||||
the result to the future. If ``None``, returns the data.
|
||||
|
||||
Returns
|
||||
--------
|
||||
asyncio.Future
|
||||
A future to wait for.
|
||||
"""
|
||||
|
||||
future = self.loop.create_future()
|
||||
entry = EventListener(event=event, predicate=predicate, result=result, future=future)
|
||||
self._dispatch_listeners.append(entry)
|
||||
return future
|
||||
|
||||
async def identify(self):
|
||||
"""Sends the IDENTIFY packet."""
|
||||
payload = {
|
||||
'op': self.IDENTIFY,
|
||||
'd': {
|
||||
'token': self.token,
|
||||
'properties': {
|
||||
'$os': sys.platform,
|
||||
'$browser': 'discord.py',
|
||||
'$device': 'discord.py',
|
||||
'$referrer': '',
|
||||
'$referring_domain': ''
|
||||
},
|
||||
'compress': True,
|
||||
'large_threshold': 250,
|
||||
'guild_subscriptions': self._connection.guild_subscriptions,
|
||||
'v': 3
|
||||
}
|
||||
}
|
||||
|
||||
if not self._connection.is_bot:
|
||||
payload['d']['synced_guilds'] = []
|
||||
|
||||
if self.shard_id is not None and self.shard_count is not None:
|
||||
payload['d']['shard'] = [self.shard_id, self.shard_count]
|
||||
|
||||
state = self._connection
|
||||
if state._activity is not None or state._status is not None:
|
||||
payload['d']['presence'] = {
|
||||
'status': state._status,
|
||||
'game': state._activity,
|
||||
'since': 0,
|
||||
'afk': False
|
||||
}
|
||||
|
||||
if state._intents is not None:
|
||||
payload['d']['intents'] = state._intents.value
|
||||
|
||||
await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify)
|
||||
await self.send_as_json(payload)
|
||||
log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id)
|
||||
|
||||
async def resume(self):
|
||||
"""Sends the RESUME packet."""
|
||||
payload = {
|
||||
'op': self.RESUME,
|
||||
'd': {
|
||||
'seq': self.sequence,
|
||||
'session_id': self.session_id,
|
||||
'token': self.token
|
||||
}
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
log.info('Shard ID %s has sent the RESUME payload.', self.shard_id)
|
||||
|
||||
async def received_message(self, msg):
|
||||
self._dispatch('socket_raw_receive', msg)
|
||||
|
||||
if type(msg) is bytes:
|
||||
self._buffer.extend(msg)
|
||||
|
||||
if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff':
|
||||
return
|
||||
msg = self._zlib.decompress(self._buffer)
|
||||
msg = msg.decode('utf-8')
|
||||
self._buffer = bytearray()
|
||||
msg = json.loads(msg)
|
||||
|
||||
log.debug('For Shard ID %s: WebSocket Event: %s', self.shard_id, msg)
|
||||
self._dispatch('socket_response', msg)
|
||||
|
||||
op = msg.get('op')
|
||||
data = msg.get('d')
|
||||
seq = msg.get('s')
|
||||
if seq is not None:
|
||||
self.sequence = seq
|
||||
|
||||
if self._keep_alive:
|
||||
self._keep_alive.tick()
|
||||
|
||||
if op != self.DISPATCH:
|
||||
if op == self.RECONNECT:
|
||||
# "reconnect" can only be handled by the Client
|
||||
# so we terminate our connection and raise an
|
||||
# internal exception signalling to reconnect.
|
||||
log.debug('Received RECONNECT opcode.')
|
||||
await self.close()
|
||||
raise ReconnectWebSocket(self.shard_id)
|
||||
|
||||
if op == self.HEARTBEAT_ACK:
|
||||
if self._keep_alive:
|
||||
self._keep_alive.ack()
|
||||
return
|
||||
|
||||
if op == self.HEARTBEAT:
|
||||
if self._keep_alive:
|
||||
beat = self._keep_alive.get_payload()
|
||||
await self.send_as_json(beat)
|
||||
return
|
||||
|
||||
if op == self.HELLO:
|
||||
interval = data['heartbeat_interval'] / 1000.0
|
||||
self._keep_alive = KeepAliveHandler(ws=self, interval=interval, shard_id=self.shard_id)
|
||||
# send a heartbeat immediately
|
||||
await self.send_as_json(self._keep_alive.get_payload())
|
||||
self._keep_alive.start()
|
||||
return
|
||||
|
||||
if op == self.INVALIDATE_SESSION:
|
||||
if data is True:
|
||||
await self.close()
|
||||
raise ReconnectWebSocket(self.shard_id)
|
||||
|
||||
self.sequence = None
|
||||
self.session_id = None
|
||||
log.info('Shard ID %s session has been invalidated.', self.shard_id)
|
||||
await self.close(code=1000)
|
||||
raise ReconnectWebSocket(self.shard_id, resume=False)
|
||||
|
||||
log.warning('Unknown OP code %s.', op)
|
||||
return
|
||||
|
||||
event = msg.get('t')
|
||||
|
||||
if event == 'READY':
|
||||
self._trace = trace = data.get('_trace', [])
|
||||
self.sequence = msg['s']
|
||||
self.session_id = data['session_id']
|
||||
# pass back shard ID to ready handler
|
||||
data['__shard_id__'] = self.shard_id
|
||||
log.info('Shard ID %s has connected to Gateway: %s (Session ID: %s).',
|
||||
self.shard_id, ', '.join(trace), self.session_id)
|
||||
|
||||
elif event == 'RESUMED':
|
||||
self._trace = trace = data.get('_trace', [])
|
||||
# pass back the shard ID to the resumed handler
|
||||
data['__shard_id__'] = self.shard_id
|
||||
log.info('Shard ID %s has successfully RESUMED session %s under trace %s.',
|
||||
self.shard_id, self.session_id, ', '.join(trace))
|
||||
|
||||
try:
|
||||
func = self._discord_parsers[event]
|
||||
except KeyError:
|
||||
log.debug('Unknown event %s.', event)
|
||||
else:
|
||||
func(data)
|
||||
|
||||
# remove the dispatched listeners
|
||||
removed = []
|
||||
for index, entry in enumerate(self._dispatch_listeners):
|
||||
if entry.event != event:
|
||||
continue
|
||||
|
||||
future = entry.future
|
||||
if future.cancelled():
|
||||
removed.append(index)
|
||||
continue
|
||||
|
||||
try:
|
||||
valid = entry.predicate(data)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
removed.append(index)
|
||||
else:
|
||||
if valid:
|
||||
ret = data if entry.result is None else entry.result(data)
|
||||
future.set_result(ret)
|
||||
removed.append(index)
|
||||
|
||||
for index in reversed(removed):
|
||||
del self._dispatch_listeners[index]
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
|
||||
heartbeat = self._keep_alive
|
||||
return float('inf') if heartbeat is None else heartbeat.latency
|
||||
|
||||
def _can_handle_close(self):
|
||||
code = self._close_code or self.socket.close_code
|
||||
return code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014)
|
||||
|
||||
async def poll_event(self):
|
||||
"""Polls for a DISPATCH event and handles the general gateway loop.
|
||||
|
||||
Raises
|
||||
------
|
||||
ConnectionClosed
|
||||
The websocket connection was terminated for unhandled reasons.
|
||||
"""
|
||||
try:
|
||||
msg = await self.socket.receive(timeout=self._max_heartbeat_timeout)
|
||||
if msg.type is aiohttp.WSMsgType.TEXT:
|
||||
await self.received_message(msg.data)
|
||||
elif msg.type is aiohttp.WSMsgType.BINARY:
|
||||
await self.received_message(msg.data)
|
||||
elif msg.type is aiohttp.WSMsgType.ERROR:
|
||||
log.debug('Received %s', msg)
|
||||
raise msg.data
|
||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSE):
|
||||
log.debug('Received %s', msg)
|
||||
raise WebSocketClosure
|
||||
except (asyncio.TimeoutError, WebSocketClosure) as e:
|
||||
# Ensure the keep alive handler is closed
|
||||
if self._keep_alive:
|
||||
self._keep_alive.stop()
|
||||
self._keep_alive = None
|
||||
|
||||
if isinstance(e, asyncio.TimeoutError):
|
||||
log.info('Timed out receiving packet. Attempting a reconnect.')
|
||||
raise ReconnectWebSocket(self.shard_id) from None
|
||||
|
||||
code = self._close_code or self.socket.close_code
|
||||
if self._can_handle_close():
|
||||
log.info('Websocket closed with %s, attempting a reconnect.', code)
|
||||
raise ReconnectWebSocket(self.shard_id) from None
|
||||
else:
|
||||
log.info('Websocket closed with %s, cannot reconnect.', code)
|
||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id, code=code) from None
|
||||
|
||||
async def send(self, data):
|
||||
await self._rate_limiter.block()
|
||||
self._dispatch('socket_raw_send', data)
|
||||
await self.socket.send_str(data)
|
||||
|
||||
async def send_as_json(self, data):
|
||||
try:
|
||||
await self.send(utils.to_json(data))
|
||||
except RuntimeError as exc:
|
||||
if not self._can_handle_close():
|
||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
||||
|
||||
async def send_heartbeat(self, data):
|
||||
# This bypasses the rate limit handling code since it has a higher priority
|
||||
try:
|
||||
await self.socket.send_str(utils.to_json(data))
|
||||
except RuntimeError as exc:
|
||||
if not self._can_handle_close():
|
||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
||||
|
||||
async def change_presence(self, *, activity=None, status=None, afk=False, since=0.0):
|
||||
if activity is not None:
|
||||
if not isinstance(activity, BaseActivity):
|
||||
raise InvalidArgument('activity must derive from BaseActivity.')
|
||||
activity = activity.to_dict()
|
||||
|
||||
if status == 'idle':
|
||||
since = int(time.time() * 1000)
|
||||
|
||||
payload = {
|
||||
'op': self.PRESENCE,
|
||||
'd': {
|
||||
'game': activity,
|
||||
'afk': afk,
|
||||
'since': since,
|
||||
'status': status
|
||||
}
|
||||
}
|
||||
|
||||
sent = utils.to_json(payload)
|
||||
log.debug('Sending "%s" to change status', sent)
|
||||
await self.send(sent)
|
||||
|
||||
async def request_sync(self, guild_ids):
|
||||
payload = {
|
||||
'op': self.GUILD_SYNC,
|
||||
'd': list(guild_ids)
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None):
|
||||
payload = {
|
||||
'op': self.REQUEST_MEMBERS,
|
||||
'd': {
|
||||
'guild_id': guild_id,
|
||||
'presences': presences,
|
||||
'limit': limit
|
||||
}
|
||||
}
|
||||
|
||||
if nonce:
|
||||
payload['d']['nonce'] = nonce
|
||||
|
||||
if user_ids:
|
||||
payload['d']['user_ids'] = user_ids
|
||||
|
||||
if query is not None:
|
||||
payload['d']['query'] = query
|
||||
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False):
|
||||
payload = {
|
||||
'op': self.VOICE_STATE,
|
||||
'd': {
|
||||
'guild_id': guild_id,
|
||||
'channel_id': channel_id,
|
||||
'self_mute': self_mute,
|
||||
'self_deaf': self_deaf
|
||||
}
|
||||
}
|
||||
|
||||
log.debug('Updating our voice state to %s.', payload)
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def close(self, code=4000):
|
||||
if self._keep_alive:
|
||||
self._keep_alive.stop()
|
||||
self._keep_alive = None
|
||||
|
||||
self._close_code = code
|
||||
await self.socket.close(code=code)
|
||||
|
||||
class DiscordVoiceWebSocket:
|
||||
"""Implements the websocket protocol for handling voice connections.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
IDENTIFY
|
||||
Send only. Starts a new voice session.
|
||||
SELECT_PROTOCOL
|
||||
Send only. Tells discord what encryption mode and how to connect for voice.
|
||||
READY
|
||||
Receive only. Tells the websocket that the initial connection has completed.
|
||||
HEARTBEAT
|
||||
Send only. Keeps your websocket connection alive.
|
||||
SESSION_DESCRIPTION
|
||||
Receive only. Gives you the secret key required for voice.
|
||||
SPEAKING
|
||||
Send only. Notifies the client if you are currently speaking.
|
||||
HEARTBEAT_ACK
|
||||
Receive only. Tells you your heartbeat has been acknowledged.
|
||||
RESUME
|
||||
Sent only. Tells the client to resume its session.
|
||||
HELLO
|
||||
Receive only. Tells you that your websocket connection was acknowledged.
|
||||
RESUMED
|
||||
Sent only. Tells you that your RESUME request has succeeded.
|
||||
CLIENT_CONNECT
|
||||
Indicates a user has connected to voice.
|
||||
CLIENT_DISCONNECT
|
||||
Receive only. Indicates a user has disconnected from voice.
|
||||
"""
|
||||
|
||||
IDENTIFY = 0
|
||||
SELECT_PROTOCOL = 1
|
||||
READY = 2
|
||||
HEARTBEAT = 3
|
||||
SESSION_DESCRIPTION = 4
|
||||
SPEAKING = 5
|
||||
HEARTBEAT_ACK = 6
|
||||
RESUME = 7
|
||||
HELLO = 8
|
||||
RESUMED = 9
|
||||
CLIENT_CONNECT = 12
|
||||
CLIENT_DISCONNECT = 13
|
||||
|
||||
def __init__(self, socket, loop):
|
||||
self.ws = socket
|
||||
self.loop = loop
|
||||
self._keep_alive = None
|
||||
self._close_code = None
|
||||
self.secret_key = None
|
||||
|
||||
async def send_as_json(self, data):
|
||||
log.debug('Sending voice websocket frame: %s.', data)
|
||||
await self.ws.send_str(utils.to_json(data))
|
||||
|
||||
send_heartbeat = send_as_json
|
||||
|
||||
async def resume(self):
|
||||
state = self._connection
|
||||
payload = {
|
||||
'op': self.RESUME,
|
||||
'd': {
|
||||
'token': state.token,
|
||||
'server_id': str(state.server_id),
|
||||
'session_id': state.session_id
|
||||
}
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def identify(self):
|
||||
state = self._connection
|
||||
payload = {
|
||||
'op': self.IDENTIFY,
|
||||
'd': {
|
||||
'server_id': str(state.server_id),
|
||||
'user_id': str(state.user.id),
|
||||
'session_id': state.session_id,
|
||||
'token': state.token
|
||||
}
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
@classmethod
|
||||
async def from_client(cls, client, *, resume=False):
|
||||
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
||||
gateway = 'wss://' + client.endpoint + '/?v=4'
|
||||
http = client._state.http
|
||||
socket = await http.ws_connect(gateway, compress=15)
|
||||
ws = cls(socket, loop=client.loop)
|
||||
ws.gateway = gateway
|
||||
ws._connection = client
|
||||
ws._max_heartbeat_timeout = 60.0
|
||||
ws.thread_id = threading.get_ident()
|
||||
|
||||
if resume:
|
||||
await ws.resume()
|
||||
else:
|
||||
await ws.identify()
|
||||
|
||||
return ws
|
||||
|
||||
async def select_protocol(self, ip, port, mode):
|
||||
payload = {
|
||||
'op': self.SELECT_PROTOCOL,
|
||||
'd': {
|
||||
'protocol': 'udp',
|
||||
'data': {
|
||||
'address': ip,
|
||||
'port': port,
|
||||
'mode': mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def client_connect(self):
|
||||
payload = {
|
||||
'op': self.CLIENT_CONNECT,
|
||||
'd': {
|
||||
'audio_ssrc': self._connection.ssrc
|
||||
}
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def speak(self, state=SpeakingState.voice):
|
||||
payload = {
|
||||
'op': self.SPEAKING,
|
||||
'd': {
|
||||
'speaking': int(state),
|
||||
'delay': 0
|
||||
}
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def received_message(self, msg):
|
||||
log.debug('Voice websocket frame received: %s', msg)
|
||||
op = msg['op']
|
||||
data = msg.get('d')
|
||||
|
||||
if op == self.READY:
|
||||
await self.initial_connection(data)
|
||||
elif op == self.HEARTBEAT_ACK:
|
||||
self._keep_alive.ack()
|
||||
elif op == self.RESUMED:
|
||||
log.info('Voice RESUME succeeded.')
|
||||
elif op == self.SESSION_DESCRIPTION:
|
||||
self._connection.mode = data['mode']
|
||||
await self.load_secret_key(data)
|
||||
elif op == self.HELLO:
|
||||
interval = data['heartbeat_interval'] / 1000.0
|
||||
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0))
|
||||
self._keep_alive.start()
|
||||
|
||||
async def initial_connection(self, data):
|
||||
state = self._connection
|
||||
state.ssrc = data['ssrc']
|
||||
state.voice_port = data['port']
|
||||
state.endpoint_ip = data['ip']
|
||||
|
||||
packet = bytearray(70)
|
||||
struct.pack_into('>H', packet, 0, 1) # 1 = Send
|
||||
struct.pack_into('>H', packet, 2, 70) # 70 = Length
|
||||
struct.pack_into('>I', packet, 4, state.ssrc)
|
||||
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
|
||||
recv = await self.loop.sock_recv(state.socket, 70)
|
||||
log.debug('received packet in initial_connection: %s', recv)
|
||||
|
||||
# the ip is ascii starting at the 4th byte and ending at the first null
|
||||
ip_start = 4
|
||||
ip_end = recv.index(0, ip_start)
|
||||
state.ip = recv[ip_start:ip_end].decode('ascii')
|
||||
|
||||
state.port = struct.unpack_from('>H', recv, len(recv) - 2)[0]
|
||||
log.debug('detected ip: %s port: %s', state.ip, state.port)
|
||||
|
||||
# there *should* always be at least one supported mode (xsalsa20_poly1305)
|
||||
modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes]
|
||||
log.debug('received supported encryption modes: %s', ", ".join(modes))
|
||||
|
||||
mode = modes[0]
|
||||
await self.select_protocol(state.ip, state.port, mode)
|
||||
log.info('selected the voice protocol for use (%s)', mode)
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""
|
||||
heartbeat = self._keep_alive
|
||||
return float('inf') if heartbeat is None else heartbeat.latency
|
||||
|
||||
@property
|
||||
def average_latency(self):
|
||||
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
||||
heartbeat = self._keep_alive
|
||||
if heartbeat is None or not heartbeat.recent_ack_latencies:
|
||||
return float('inf')
|
||||
|
||||
return sum(heartbeat.recent_ack_latencies) / len(heartbeat.recent_ack_latencies)
|
||||
|
||||
async def load_secret_key(self, data):
|
||||
log.info('received secret key for voice connection')
|
||||
self.secret_key = self._connection.secret_key = data.get('secret_key')
|
||||
await self.speak()
|
||||
await self.speak(False)
|
||||
|
||||
async def poll_event(self):
|
||||
# This exception is handled up the chain
|
||||
msg = await asyncio.wait_for(self.ws.receive(), timeout=30.0)
|
||||
if msg.type is aiohttp.WSMsgType.TEXT:
|
||||
await self.received_message(json.loads(msg.data))
|
||||
elif msg.type is aiohttp.WSMsgType.ERROR:
|
||||
log.debug('Received %s', msg)
|
||||
raise ConnectionClosed(self.ws, shard_id=None) from msg.data
|
||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING):
|
||||
log.debug('Received %s', msg)
|
||||
raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code)
|
||||
|
||||
async def close(self, code=1000):
|
||||
if self._keep_alive is not None:
|
||||
self._keep_alive.stop()
|
||||
|
||||
self._close_code = code
|
||||
await self.ws.close(code=code)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,206 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from .utils import _get_as_snowflake, get, parse_time
|
||||
from .user import User
|
||||
from .errors import InvalidArgument
|
||||
from .enums import try_enum, ExpireBehaviour
|
||||
|
||||
class IntegrationAccount:
|
||||
"""Represents an integration account.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The account ID.
|
||||
name: :class:`str`
|
||||
The account name.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
|
||||
def __repr__(self):
|
||||
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)
|
||||
|
||||
class Integration:
|
||||
"""Represents a guild integration.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The integration ID.
|
||||
name: :class:`str`
|
||||
The integration name.
|
||||
guild: :class:`Guild`
|
||||
The guild of the integration.
|
||||
type: :class:`str`
|
||||
The integration type (i.e. Twitch).
|
||||
enabled: :class:`bool`
|
||||
Whether the integration is currently enabled.
|
||||
syncing: :class:`bool`
|
||||
Where the integration is currently syncing.
|
||||
role: :class:`Role`
|
||||
The role which the integration uses for subscribers.
|
||||
enable_emoticons: Optional[:class:`bool`]
|
||||
Whether emoticons should be synced for this integration (currently twitch only).
|
||||
expire_behaviour: :class:`ExpireBehaviour`
|
||||
The behaviour of expiring subscribers. Aliased to ``expire_behavior`` as well.
|
||||
expire_grace_period: :class:`int`
|
||||
The grace period (in days) for expiring subscribers.
|
||||
user: :class:`User`
|
||||
The user for the integration.
|
||||
account: :class:`IntegrationAccount`
|
||||
The integration account information.
|
||||
synced_at: :class:`datetime.datetime`
|
||||
When the integration was last synced.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
|
||||
'syncing', 'role', 'expire_behaviour', 'expire_behavior',
|
||||
'expire_grace_period', 'synced_at', 'user', 'account',
|
||||
'enable_emoticons', '_role_id')
|
||||
|
||||
def __init__(self, *, data, guild):
|
||||
self.guild = guild
|
||||
self._state = guild._state
|
||||
self._from_data(data)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Integration id={0.id} name={0.name!r} type={0.type!r}>'.format(self)
|
||||
|
||||
def _from_data(self, integ):
|
||||
self.id = _get_as_snowflake(integ, 'id')
|
||||
self.name = integ['name']
|
||||
self.type = integ['type']
|
||||
self.enabled = integ['enabled']
|
||||
self.syncing = integ['syncing']
|
||||
self._role_id = _get_as_snowflake(integ, 'role_id')
|
||||
self.role = get(self.guild.roles, id=self._role_id)
|
||||
self.enable_emoticons = integ.get('enable_emoticons')
|
||||
self.expire_behaviour = try_enum(ExpireBehaviour, integ['expire_behavior'])
|
||||
self.expire_behavior = self.expire_behaviour
|
||||
self.expire_grace_period = integ['expire_grace_period']
|
||||
self.synced_at = parse_time(integ['synced_at'])
|
||||
|
||||
self.user = User(state=self._state, data=integ['user'])
|
||||
self.account = IntegrationAccount(**integ['account'])
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the integration.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
expire_behaviour: :class:`ExpireBehaviour`
|
||||
The behaviour when an integration subscription lapses. Aliased to ``expire_behavior`` as well.
|
||||
expire_grace_period: :class:`int`
|
||||
The period (in days) where the integration will ignore lapsed subscriptions.
|
||||
enable_emoticons: :class:`bool`
|
||||
Where emoticons should be synced for this integration (currently twitch only).
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to edit the integration.
|
||||
HTTPException
|
||||
Editing the guild failed.
|
||||
InvalidArgument
|
||||
``expire_behaviour`` did not receive a :class:`ExpireBehaviour`.
|
||||
"""
|
||||
try:
|
||||
expire_behaviour = fields['expire_behaviour']
|
||||
except KeyError:
|
||||
expire_behaviour = fields.get('expire_behavior', self.expire_behaviour)
|
||||
|
||||
if not isinstance(expire_behaviour, ExpireBehaviour):
|
||||
raise InvalidArgument('expire_behaviour field must be of type ExpireBehaviour')
|
||||
|
||||
expire_grace_period = fields.get('expire_grace_period', self.expire_grace_period)
|
||||
|
||||
payload = {
|
||||
'expire_behavior': expire_behaviour.value,
|
||||
'expire_grace_period': expire_grace_period,
|
||||
}
|
||||
|
||||
enable_emoticons = fields.get('enable_emoticons')
|
||||
|
||||
if enable_emoticons is not None:
|
||||
payload['enable_emoticons'] = enable_emoticons
|
||||
|
||||
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
|
||||
|
||||
self.expire_behaviour = expire_behaviour
|
||||
self.expire_behavior = self.expire_behaviour
|
||||
self.expire_grace_period = expire_grace_period
|
||||
self.enable_emoticons = enable_emoticons
|
||||
|
||||
async def sync(self):
|
||||
"""|coro|
|
||||
|
||||
Syncs the integration.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to sync the integration.
|
||||
HTTPException
|
||||
Syncing the integration failed.
|
||||
"""
|
||||
await self._state.http.sync_integration(self.guild.id, self.id)
|
||||
self.synced_at = datetime.datetime.utcnow()
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the integration.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to delete the integration.
|
||||
HTTPException
|
||||
Deleting the integration failed.
|
||||
"""
|
||||
await self._state.http.delete_integration(self.guild.id, self.id)
|
@@ -1,399 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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 .asset import Asset
|
||||
from .utils import parse_time, snowflake_time, _get_as_snowflake
|
||||
from .object import Object
|
||||
from .mixins import Hashable
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
|
||||
class PartialInviteChannel:
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
This model will be given when the user is not part of the
|
||||
guild the :class:`Invite` resolves to.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two partial channels are the same.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two partial channels are not the same.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the partial channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the partial channel's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The partial channel's name.
|
||||
id: :class:`int`
|
||||
The partial channel's ID.
|
||||
type: :class:`ChannelType`
|
||||
The partial channel's type.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name', 'type')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
self.type = kwargs.pop('type')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
class PartialInviteGuild:
|
||||
"""Represents a "partial" invite guild.
|
||||
|
||||
This model will be given when the user is not part of the
|
||||
guild the :class:`Invite` resolves to.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two partial guilds are the same.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two partial guilds are not the same.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the partial guild's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the partial guild's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The partial guild's name.
|
||||
id: :class:`int`
|
||||
The partial guild's ID.
|
||||
verification_level: :class:`VerificationLevel`
|
||||
The partial guild's verification level.
|
||||
features: List[:class:`str`]
|
||||
A list of features the guild has. See :attr:`Guild.features` for more information.
|
||||
icon: Optional[:class:`str`]
|
||||
The partial guild's icon.
|
||||
banner: Optional[:class:`str`]
|
||||
The partial guild's banner.
|
||||
splash: Optional[:class:`str`]
|
||||
The partial guild's invite splash.
|
||||
description: Optional[:class:`str`]
|
||||
The partial guild's description.
|
||||
"""
|
||||
|
||||
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash',
|
||||
'verification_level', 'description')
|
||||
|
||||
def __init__(self, state, data, id):
|
||||
self._state = state
|
||||
self.id = id
|
||||
self.name = data['name']
|
||||
self.features = data.get('features', [])
|
||||
self.icon = data.get('icon')
|
||||
self.banner = data.get('banner')
|
||||
self.splash = data.get('splash')
|
||||
self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
|
||||
self.description = data.get('description')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} features={0.features} ' \
|
||||
'description={0.description!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`Asset`: Returns the guild's icon asset."""
|
||||
return self.icon_url_as()
|
||||
|
||||
def is_icon_animated(self):
|
||||
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
return bool(self.icon and self.icon.startswith('a_'))
|
||||
|
||||
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
|
||||
"""The same operation as :meth:`Guild.icon_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
|
||||
|
||||
@property
|
||||
def banner_url(self):
|
||||
""":class:`Asset`: Returns the guild's banner asset."""
|
||||
return self.banner_url_as()
|
||||
|
||||
def banner_url_as(self, *, format='webp', size=2048):
|
||||
"""The same operation as :meth:`Guild.banner_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
|
||||
|
||||
@property
|
||||
def splash_url(self):
|
||||
""":class:`Asset`: Returns the guild's invite splash asset."""
|
||||
return self.splash_url_as()
|
||||
|
||||
def splash_url_as(self, *, format='webp', size=2048):
|
||||
"""The same operation as :meth:`Guild.splash_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
|
||||
|
||||
class Invite(Hashable):
|
||||
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two invites are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two invites are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the invite hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the invite URL.
|
||||
|
||||
The following table illustrates what methods will obtain the attributes:
|
||||
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| Attribute | Method |
|
||||
+====================================+==========================================================+
|
||||
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
|
||||
If it's not in the table above then it is available by all methods.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
max_age: :class:`int`
|
||||
How long the before the invite expires in seconds.
|
||||
A value of ``0`` indicates that it doesn't expire.
|
||||
code: :class:`str`
|
||||
The URL fragment used for the invite.
|
||||
guild: Optional[Union[:class:`Guild`, :class:`Object`, :class:`PartialInviteGuild`]]
|
||||
The guild the invite is for. Can be ``None`` if it's from a group direct message.
|
||||
revoked: :class:`bool`
|
||||
Indicates if the invite has been revoked.
|
||||
created_at: :class:`datetime.datetime`
|
||||
A datetime object denoting the time the invite was created.
|
||||
temporary: :class:`bool`
|
||||
Indicates that the invite grants temporary membership.
|
||||
If ``True``, members who joined via this invite will be kicked upon disconnect.
|
||||
uses: :class:`int`
|
||||
How many times the invite has been used.
|
||||
max_uses: :class:`int`
|
||||
How many times the invite can be used.
|
||||
A value of ``0`` indicates that it has unlimited uses.
|
||||
inviter: :class:`User`
|
||||
The user who created the invite.
|
||||
approximate_member_count: Optional[:class:`int`]
|
||||
The approximate number of members in the guild.
|
||||
approximate_presence_count: Optional[:class:`int`]
|
||||
The approximate number of members currently active in the guild.
|
||||
This includes idle, dnd, online, and invisible members. Offline members are excluded.
|
||||
channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]
|
||||
The channel the invite is for.
|
||||
"""
|
||||
|
||||
__slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses',
|
||||
'temporary', 'max_uses', 'inviter', 'channel', '_state',
|
||||
'approximate_member_count', 'approximate_presence_count' )
|
||||
|
||||
BASE = 'https://discord.gg'
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.max_age = data.get('max_age')
|
||||
self.code = data.get('code')
|
||||
self.guild = data.get('guild')
|
||||
self.revoked = data.get('revoked')
|
||||
self.created_at = parse_time(data.get('created_at'))
|
||||
self.temporary = data.get('temporary')
|
||||
self.uses = data.get('uses')
|
||||
self.max_uses = data.get('max_uses')
|
||||
self.approximate_presence_count = data.get('approximate_presence_count')
|
||||
self.approximate_member_count = data.get('approximate_member_count')
|
||||
|
||||
inviter_data = data.get('inviter')
|
||||
self.inviter = None if inviter_data is None else self._state.store_user(inviter_data)
|
||||
self.channel = data.get('channel')
|
||||
|
||||
@classmethod
|
||||
def from_incomplete(cls, *, state, data):
|
||||
try:
|
||||
guild_id = int(data['guild']['id'])
|
||||
except KeyError:
|
||||
# If we're here, then this is a group DM
|
||||
guild = None
|
||||
else:
|
||||
guild = state._get_guild(guild_id)
|
||||
if guild is None:
|
||||
# If it's not cached, then it has to be a partial guild
|
||||
guild_data = data['guild']
|
||||
guild = PartialInviteGuild(state, guild_data, guild_id)
|
||||
|
||||
# As far as I know, invites always need a channel
|
||||
# So this should never raise.
|
||||
channel_data = data['channel']
|
||||
channel_id = int(channel_data['id'])
|
||||
channel_type = try_enum(ChannelType, channel_data['type'])
|
||||
channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type)
|
||||
if guild is not None and not isinstance(guild, PartialInviteGuild):
|
||||
# Upgrade the partial data if applicable
|
||||
channel = guild.get_channel(channel_id) or channel
|
||||
|
||||
data['guild'] = guild
|
||||
data['channel'] = channel
|
||||
return cls(state=state, data=data)
|
||||
|
||||
@classmethod
|
||||
def from_gateway(cls, *, state, data):
|
||||
guild_id = _get_as_snowflake(data, 'guild_id')
|
||||
guild = state._get_guild(guild_id)
|
||||
channel_id = _get_as_snowflake(data, 'channel_id')
|
||||
if guild is not None:
|
||||
channel = guild.get_channel(channel_id) or Object(id=channel_id)
|
||||
else:
|
||||
guild = Object(id=guild_id)
|
||||
channel = Object(id=channel_id)
|
||||
|
||||
data['guild'] = guild
|
||||
data['channel'] = channel
|
||||
return cls(state=state, data=data)
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
def __repr__(self):
|
||||
return '<Invite code={0.code!r} guild={0.guild!r} ' \
|
||||
'online={0.approximate_presence_count} ' \
|
||||
'members={0.approximate_member_count}>'.format(self)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.code)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
""":class:`str`: Returns the proper code portion of the invite."""
|
||||
return self.code
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`str`: A property that retrieves the invite URL."""
|
||||
return self.BASE + '/' + self.code
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Revokes the instant invite.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for deleting this invite. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to revoke invites.
|
||||
NotFound
|
||||
The invite is invalid or expired.
|
||||
HTTPException
|
||||
Revoking the invite failed.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_invite(self.code, reason=reason)
|
@@ -1,655 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
from .errors import NoMoreItems
|
||||
from .utils import time_snowflake, maybe_coroutine
|
||||
from .object import Object
|
||||
from .audit_logs import AuditLogEntry
|
||||
|
||||
OLDEST_OBJECT = Object(id=0)
|
||||
|
||||
class _AsyncIterator:
|
||||
__slots__ = ()
|
||||
|
||||
def get(self, **attrs):
|
||||
def predicate(elem):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split('__')
|
||||
obj = elem
|
||||
for attribute in nested:
|
||||
obj = getattr(obj, attribute)
|
||||
|
||||
if obj != val:
|
||||
return False
|
||||
return True
|
||||
|
||||
return self.find(predicate)
|
||||
|
||||
async def find(self, predicate):
|
||||
while True:
|
||||
try:
|
||||
elem = await self.next()
|
||||
except NoMoreItems:
|
||||
return None
|
||||
|
||||
ret = await maybe_coroutine(predicate, elem)
|
||||
if ret:
|
||||
return elem
|
||||
|
||||
def chunk(self, max_size):
|
||||
if max_size <= 0:
|
||||
raise ValueError('async iterator chunk sizes must be greater than 0.')
|
||||
return _ChunkedAsyncIterator(self, max_size)
|
||||
|
||||
def map(self, func):
|
||||
return _MappedAsyncIterator(self, func)
|
||||
|
||||
def filter(self, predicate):
|
||||
return _FilteredAsyncIterator(self, predicate)
|
||||
|
||||
async def flatten(self):
|
||||
ret = []
|
||||
while True:
|
||||
try:
|
||||
item = await self.next()
|
||||
except NoMoreItems:
|
||||
return ret
|
||||
else:
|
||||
ret.append(item)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
msg = await self.next()
|
||||
except NoMoreItems:
|
||||
raise StopAsyncIteration()
|
||||
else:
|
||||
return msg
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
class _ChunkedAsyncIterator(_AsyncIterator):
|
||||
def __init__(self, iterator, max_size):
|
||||
self.iterator = iterator
|
||||
self.max_size = max_size
|
||||
|
||||
async def next(self):
|
||||
ret = []
|
||||
n = 0
|
||||
while n < self.max_size:
|
||||
try:
|
||||
item = await self.iterator.next()
|
||||
except NoMoreItems:
|
||||
if ret:
|
||||
return ret
|
||||
raise
|
||||
else:
|
||||
ret.append(item)
|
||||
n += 1
|
||||
return ret
|
||||
|
||||
class _MappedAsyncIterator(_AsyncIterator):
|
||||
def __init__(self, iterator, func):
|
||||
self.iterator = iterator
|
||||
self.func = func
|
||||
|
||||
async def next(self):
|
||||
# this raises NoMoreItems and will propagate appropriately
|
||||
item = await self.iterator.next()
|
||||
return await maybe_coroutine(self.func, item)
|
||||
|
||||
class _FilteredAsyncIterator(_AsyncIterator):
|
||||
def __init__(self, iterator, predicate):
|
||||
self.iterator = iterator
|
||||
|
||||
if predicate is None:
|
||||
predicate = _identity
|
||||
|
||||
self.predicate = predicate
|
||||
|
||||
async def next(self):
|
||||
getter = self.iterator.next
|
||||
pred = self.predicate
|
||||
while True:
|
||||
# propagate NoMoreItems similar to _MappedAsyncIterator
|
||||
item = await getter()
|
||||
ret = await maybe_coroutine(pred, item)
|
||||
if ret:
|
||||
return item
|
||||
|
||||
class ReactionIterator(_AsyncIterator):
|
||||
def __init__(self, message, emoji, limit=100, after=None):
|
||||
self.message = message
|
||||
self.limit = limit
|
||||
self.after = after
|
||||
state = message._state
|
||||
self.getter = state.http.get_reaction_users
|
||||
self.state = state
|
||||
self.emoji = emoji
|
||||
self.guild = message.guild
|
||||
self.channel_id = message.channel.id
|
||||
self.users = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
if self.users.empty():
|
||||
await self.fill_users()
|
||||
|
||||
try:
|
||||
return self.users.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
async def fill_users(self):
|
||||
# this is a hack because >circular imports<
|
||||
from .user import User
|
||||
|
||||
if self.limit > 0:
|
||||
retrieve = self.limit if self.limit <= 100 else 100
|
||||
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.getter(self.channel_id, self.message.id, self.emoji, retrieve, after=after)
|
||||
|
||||
if data:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[-1]['id']))
|
||||
|
||||
if self.guild is None or isinstance(self.guild, Object):
|
||||
for element in reversed(data):
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
else:
|
||||
for element in reversed(data):
|
||||
member_id = int(element['id'])
|
||||
member = self.guild.get_member(member_id)
|
||||
if member is not None:
|
||||
await self.users.put(member)
|
||||
else:
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
|
||||
class HistoryIterator(_AsyncIterator):
|
||||
"""Iterator for receiving a channel's message history.
|
||||
|
||||
The messages endpoint has two behaviours we care about here:
|
||||
If ``before`` is specified, the messages endpoint returns the `limit`
|
||||
newest messages before ``before``, sorted with newest first. For filling over
|
||||
100 messages, update the ``before`` parameter to the oldest message received.
|
||||
Messages will be returned in order by time.
|
||||
If ``after`` is specified, it returns the ``limit`` oldest messages after
|
||||
``after``, sorted with newest first. For filling over 100 messages, update the
|
||||
``after`` parameter to the newest message received. If messages are not
|
||||
reversed, they will be out of order (99-0, 199-100, so on)
|
||||
|
||||
A note that if both ``before`` and ``after`` are specified, ``before`` is ignored by the
|
||||
messages endpoint.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
messageable: :class:`abc.Messageable`
|
||||
Messageable class to retrieve message history from.
|
||||
limit: :class:`int`
|
||||
Maximum number of messages to retrieve
|
||||
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Message before which all messages must be.
|
||||
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Message after which all messages must be.
|
||||
around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Message around which all messages must be. Limit max 101. Note that if
|
||||
limit is an even number, this will return at most limit+1 messages.
|
||||
oldest_first: Optional[:class:`bool`]
|
||||
If set to ``True``, return messages in oldest->newest order. Defaults to
|
||||
``True`` if `after` is specified, otherwise ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, messageable, limit,
|
||||
before=None, after=None, around=None, oldest_first=None):
|
||||
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
if isinstance(after, datetime.datetime):
|
||||
after = Object(id=time_snowflake(after, high=True))
|
||||
if isinstance(around, datetime.datetime):
|
||||
around = Object(id=time_snowflake(around))
|
||||
|
||||
if oldest_first is None:
|
||||
self.reverse = after is not None
|
||||
else:
|
||||
self.reverse = oldest_first
|
||||
|
||||
self.messageable = messageable
|
||||
self.limit = limit
|
||||
self.before = before
|
||||
self.after = after or OLDEST_OBJECT
|
||||
self.around = around
|
||||
|
||||
self._filter = None # message dict -> bool
|
||||
|
||||
self.state = self.messageable._state
|
||||
self.logs_from = self.state.http.logs_from
|
||||
self.messages = asyncio.Queue()
|
||||
|
||||
if self.around:
|
||||
if self.limit is None:
|
||||
raise ValueError('history does not support around with limit=None')
|
||||
if self.limit > 101:
|
||||
raise ValueError("history max limit 101 when specifying around parameter")
|
||||
elif self.limit == 101:
|
||||
self.limit = 100 # Thanks discord
|
||||
|
||||
self._retrieve_messages = self._retrieve_messages_around_strategy
|
||||
if self.before and self.after:
|
||||
self._filter = lambda m: self.after.id < int(m['id']) < self.before.id
|
||||
elif self.before:
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
elif self.after:
|
||||
self._filter = lambda m: self.after.id < int(m['id'])
|
||||
else:
|
||||
if self.reverse:
|
||||
self._retrieve_messages = self._retrieve_messages_after_strategy
|
||||
if (self.before):
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
else:
|
||||
self._retrieve_messages = self._retrieve_messages_before_strategy
|
||||
if (self.after and self.after != OLDEST_OBJECT):
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
|
||||
async def next(self):
|
||||
if self.messages.empty():
|
||||
await self.fill_messages()
|
||||
|
||||
try:
|
||||
return self.messages.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
else:
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def flatten(self):
|
||||
# this is similar to fill_messages except it uses a list instead
|
||||
# of a queue to place the messages in.
|
||||
result = []
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
while self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
result.append(self.state.create_message(channel=channel, data=element))
|
||||
return result
|
||||
|
||||
async def fill_messages(self):
|
||||
if not hasattr(self, 'channel'):
|
||||
# do the required set up
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
|
||||
if self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
channel = self.channel
|
||||
for element in data:
|
||||
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
||||
|
||||
async def _retrieve_messages(self, retrieve):
|
||||
"""Retrieve messages and update next parameters."""
|
||||
pass
|
||||
|
||||
async def _retrieve_messages_before_strategy(self, retrieve):
|
||||
"""Retrieve messages using before parameter."""
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, before=before)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(data[-1]['id']))
|
||||
return data
|
||||
|
||||
async def _retrieve_messages_after_strategy(self, retrieve):
|
||||
"""Retrieve messages using after parameter."""
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, after=after)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
return data
|
||||
|
||||
async def _retrieve_messages_around_strategy(self, retrieve):
|
||||
"""Retrieve messages using around parameter."""
|
||||
if self.around:
|
||||
around = self.around.id if self.around else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, around=around)
|
||||
self.around = None
|
||||
return data
|
||||
return []
|
||||
|
||||
class AuditLogIterator(_AsyncIterator):
|
||||
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
if isinstance(after, datetime.datetime):
|
||||
after = Object(id=time_snowflake(after, high=True))
|
||||
|
||||
|
||||
if oldest_first is None:
|
||||
self.reverse = after is not None
|
||||
else:
|
||||
self.reverse = oldest_first
|
||||
|
||||
self.guild = guild
|
||||
self.loop = guild._state.loop
|
||||
self.request = guild._state.http.get_audit_logs
|
||||
self.limit = limit
|
||||
self.before = before
|
||||
self.user_id = user_id
|
||||
self.action_type = action_type
|
||||
self.after = OLDEST_OBJECT
|
||||
self._users = {}
|
||||
self._state = guild._state
|
||||
|
||||
|
||||
self._filter = None # entry dict -> bool
|
||||
|
||||
self.entries = asyncio.Queue()
|
||||
|
||||
|
||||
if self.reverse:
|
||||
self._strategy = self._after_strategy
|
||||
if self.before:
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
else:
|
||||
self._strategy = self._before_strategy
|
||||
if self.after and self.after != OLDEST_OBJECT:
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
|
||||
async def _before_strategy(self, retrieve):
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
||||
action_type=self.action_type, before=before)
|
||||
|
||||
entries = data.get('audit_log_entries', [])
|
||||
if len(data) and entries:
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(entries[-1]['id']))
|
||||
return data.get('users', []), entries
|
||||
|
||||
async def _after_strategy(self, retrieve):
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
||||
action_type=self.action_type, after=after)
|
||||
entries = data.get('audit_log_entries', [])
|
||||
if len(data) and entries:
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(entries[0]['id']))
|
||||
return data.get('users', []), entries
|
||||
|
||||
async def next(self):
|
||||
if self.entries.empty():
|
||||
await self._fill()
|
||||
|
||||
try:
|
||||
return self.entries.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
else:
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def _fill(self):
|
||||
from .user import User
|
||||
|
||||
if self._get_retrieve():
|
||||
users, data = await self._strategy(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for user in users:
|
||||
u = User(data=user, state=self._state)
|
||||
self._users[u.id] = u
|
||||
|
||||
for element in data:
|
||||
# TODO: remove this if statement later
|
||||
if element['action_type'] is None:
|
||||
continue
|
||||
|
||||
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
|
||||
|
||||
|
||||
class GuildIterator(_AsyncIterator):
|
||||
"""Iterator for receiving the client's guilds.
|
||||
|
||||
The guilds endpoint has the same two behaviours as described
|
||||
in :class:`HistoryIterator`:
|
||||
If ``before`` is specified, the guilds endpoint returns the ``limit``
|
||||
newest guilds before ``before``, sorted with newest first. For filling over
|
||||
100 guilds, update the ``before`` parameter to the oldest guild received.
|
||||
Guilds will be returned in order by time.
|
||||
If `after` is specified, it returns the ``limit`` oldest guilds after ``after``,
|
||||
sorted with newest first. For filling over 100 guilds, update the ``after``
|
||||
parameter to the newest guild received, If guilds are not reversed, they
|
||||
will be out of order (99-0, 199-100, so on)
|
||||
|
||||
Not that if both ``before`` and ``after`` are specified, ``before`` is ignored by the
|
||||
guilds endpoint.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
bot: :class:`discord.Client`
|
||||
The client to retrieve the guilds from.
|
||||
limit: :class:`int`
|
||||
Maximum number of guilds to retrieve.
|
||||
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Object before which all guilds must be.
|
||||
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Object after which all guilds must be.
|
||||
"""
|
||||
def __init__(self, bot, limit, before=None, after=None):
|
||||
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
if isinstance(after, datetime.datetime):
|
||||
after = Object(id=time_snowflake(after, high=True))
|
||||
|
||||
self.bot = bot
|
||||
self.limit = limit
|
||||
self.before = before
|
||||
self.after = after
|
||||
|
||||
self._filter = None
|
||||
|
||||
self.state = self.bot._connection
|
||||
self.get_guilds = self.bot.http.get_guilds
|
||||
self.guilds = asyncio.Queue()
|
||||
|
||||
if self.before and self.after:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
elif self.after:
|
||||
self._retrieve_guilds = self._retrieve_guilds_after_strategy
|
||||
else:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
|
||||
async def next(self):
|
||||
if self.guilds.empty():
|
||||
await self.fill_guilds()
|
||||
|
||||
try:
|
||||
return self.guilds.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
else:
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
def create_guild(self, data):
|
||||
from .guild import Guild
|
||||
return Guild(state=self.state, data=data)
|
||||
|
||||
async def flatten(self):
|
||||
result = []
|
||||
while self._get_retrieve():
|
||||
data = await self._retrieve_guilds(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0
|
||||
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
result.append(self.create_guild(element))
|
||||
return result
|
||||
|
||||
async def fill_guilds(self):
|
||||
if self._get_retrieve():
|
||||
data = await self._retrieve_guilds(self.retrieve)
|
||||
if self.limit is None or len(data) < 100:
|
||||
self.limit = 0
|
||||
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
await self.guilds.put(self.create_guild(element))
|
||||
|
||||
async def _retrieve_guilds(self, retrieve):
|
||||
"""Retrieve guilds and update next parameters."""
|
||||
pass
|
||||
|
||||
async def _retrieve_guilds_before_strategy(self, retrieve):
|
||||
"""Retrieve guilds using before parameter."""
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.get_guilds(retrieve, before=before)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(data[-1]['id']))
|
||||
return data
|
||||
|
||||
async def _retrieve_guilds_after_strategy(self, retrieve):
|
||||
"""Retrieve guilds using after parameter."""
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.get_guilds(retrieve, after=after)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
return data
|
||||
|
||||
class MemberIterator(_AsyncIterator):
|
||||
def __init__(self, guild, limit=1000, after=None):
|
||||
|
||||
if isinstance(after, datetime.datetime):
|
||||
after = Object(id=time_snowflake(after, high=True))
|
||||
|
||||
self.guild = guild
|
||||
self.limit = limit
|
||||
self.after = after or OLDEST_OBJECT
|
||||
|
||||
self.state = self.guild._state
|
||||
self.get_members = self.state.http.get_members
|
||||
self.members = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
if self.members.empty():
|
||||
await self.fill_members()
|
||||
|
||||
try:
|
||||
return self.members.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None or l > 1000:
|
||||
r = 1000
|
||||
else:
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def fill_members(self):
|
||||
if self._get_retrieve():
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.get_members(self.guild.id, self.retrieve, after)
|
||||
if not data:
|
||||
# no data, terminate
|
||||
return
|
||||
|
||||
if len(data) < 1000:
|
||||
self.limit = 0 # terminate loop
|
||||
|
||||
self.after = Object(id=int(data[-1]['user']['id']))
|
||||
|
||||
for element in reversed(data):
|
||||
await self.members.put(self.create_member(element))
|
||||
|
||||
def create_member(self, data):
|
||||
from .member import Member
|
||||
return Member(data=data, guild=self.guild, state=self.state)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user