Delete .venv directory

This commit is contained in:
Untriex Programming
2021-08-31 22:15:54 +02:00
committed by GitHub
parent 7795984d81
commit 5a2693bd9f
5105 changed files with 0 additions and 1440072 deletions

View File

@@ -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())

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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 *

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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