conflict fixes
This commit is contained in:
@ -1,12 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Discord API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A basic wrapper for the Discord API.
|
||||
|
||||
:copyright: (c) 2015-2020 Rapptz
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
@ -14,54 +12,53 @@ A basic wrapper for the Discord API.
|
||||
__title__ = 'discord'
|
||||
__author__ = 'Rapptz'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2015-2020 Rapptz'
|
||||
__version__ = '1.6.0.7'
|
||||
__copyright__ = 'Copyright 2015-present Rapptz'
|
||||
__version__ = '2.0.0.7a'
|
||||
|
||||
__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 .client import *
|
||||
from .appinfo import *
|
||||
from .user import *
|
||||
from .emoji import *
|
||||
from .partial_emoji import *
|
||||
from .activity import *
|
||||
from .channel import *
|
||||
from .guild import Guild
|
||||
from .guild import *
|
||||
from .flags import *
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .member import *
|
||||
from .message import *
|
||||
from .asset import Asset
|
||||
from .asset import *
|
||||
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 .permissions import *
|
||||
from .role import *
|
||||
from .file import *
|
||||
from .colour import *
|
||||
from .integrations import *
|
||||
from .invite import *
|
||||
from .template import *
|
||||
from .widget import *
|
||||
from .object import *
|
||||
from .reaction import *
|
||||
from . import utils, opus, abc
|
||||
from .enums import *
|
||||
from .embeds import Embed
|
||||
from .mentions import AllowedMentions
|
||||
from .shard import AutoShardedClient, ShardInfo
|
||||
from .embeds import *
|
||||
from .mentions import *
|
||||
from .shard import *
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
|
||||
from .voice_client import *
|
||||
from .audit_logs import *
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
from .sticker import Sticker
|
||||
from .sticker import *
|
||||
from .interactions import *
|
||||
|
||||
VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial')
|
||||
|
||||
version_info = VersionInfo(major=1, minor=6, micro=0, enhanced=7, releaselevel='alpha', serial=0)
|
||||
version_info = VersionInfo(major=2, minor=0, micro=0, enhanced=7, releaselevel='alpha', serial=0)
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -42,9 +40,9 @@ def show_version():
|
||||
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(f' - discord.py pkg_resources: v{pkg.version}')
|
||||
|
||||
entries.append('- aiohttp v{0.__version__}'.format(aiohttp))
|
||||
entries.append(f'- aiohttp v{aiohttp.__version__}')
|
||||
uname = platform.uname()
|
||||
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
|
||||
print('\n'.join(entries))
|
||||
@ -54,7 +52,6 @@ def core(parser, args):
|
||||
show_version()
|
||||
|
||||
bot_template = """#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
@ -110,9 +107,7 @@ var/
|
||||
config.py
|
||||
"""
|
||||
|
||||
cog_template = '''# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
cog_template = '''from discord.ext import commands
|
||||
import discord
|
||||
|
||||
class {name}(commands.Cog{attrs}):
|
||||
@ -200,7 +195,7 @@ def newbot(parser, args):
|
||||
try:
|
||||
new_directory.mkdir(exist_ok=True, parents=True)
|
||||
except OSError as exc:
|
||||
parser.error('could not create our bot directory ({})'.format(exc))
|
||||
parser.error(f'could not create our bot directory ({exc})')
|
||||
|
||||
cogs = new_directory / 'cogs'
|
||||
|
||||
@ -209,27 +204,27 @@ def newbot(parser, args):
|
||||
init = cogs / '__init__.py'
|
||||
init.touch()
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(exc))
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
|
||||
fp.write('token = "place your token here"\ncogs = []\n')
|
||||
except OSError as exc:
|
||||
parser.error('could not create config file ({})'.format(exc))
|
||||
parser.error(f'could not create config file ({exc})')
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp:
|
||||
base = 'Bot' if not args.sharded else 'AutoShardedBot'
|
||||
fp.write(bot_template.format(base=base, prefix=args.prefix))
|
||||
except OSError as exc:
|
||||
parser.error('could not create bot file ({})'.format(exc))
|
||||
parser.error(f'could not create bot file ({exc})')
|
||||
|
||||
if not args.no_git:
|
||||
try:
|
||||
with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
|
||||
fp.write(gitignore_template)
|
||||
except OSError as exc:
|
||||
print('warning: could not create .gitignore file ({})'.format(exc))
|
||||
print(f'warning: could not create .gitignore file ({exc})')
|
||||
|
||||
print('successfully made bot at', new_directory)
|
||||
|
||||
@ -238,7 +233,7 @@ def newcog(parser, args):
|
||||
try:
|
||||
cog_dir.mkdir(exist_ok=True)
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(exc))
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
|
||||
directory = cog_dir / to_path(parser, args.name)
|
||||
directory = directory.with_suffix('.py')
|
||||
@ -250,18 +245,19 @@ def newcog(parser, args):
|
||||
name = args.class_name
|
||||
else:
|
||||
name = str(directory.stem)
|
||||
if '-' in name:
|
||||
name = name.replace('-', ' ').title().replace(' ', '')
|
||||
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)
|
||||
attrs += f', name="{args.display_name}"'
|
||||
if args.hide_commands:
|
||||
attrs += ', command_attrs=dict(hidden=True)'
|
||||
fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
|
||||
except OSError as exc:
|
||||
parser.error('could not create cog file ({})'.format(exc))
|
||||
parser.error(f'could not create cog file ({exc})')
|
||||
else:
|
||||
print('successfully made cog at', directory)
|
||||
|
||||
@ -300,4 +296,5 @@ def main():
|
||||
parser, args = parse_args()
|
||||
args.func(parser, args)
|
||||
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
280
discord/abc.py
280
discord/abc.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,15 +22,18 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import abc
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
||||
|
||||
from .iterators import HistoryIterator
|
||||
from .context_managers import Typing
|
||||
from .enums import ChannelType
|
||||
from .errors import InvalidArgument, ClientException
|
||||
from .mentions import AllowedMentions
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .role import Role
|
||||
from .invite import Invite
|
||||
@ -40,13 +41,31 @@ from .file import File
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'Snowflake',
|
||||
'User',
|
||||
'PrivateChannel',
|
||||
'GuildChannel',
|
||||
'Messageable',
|
||||
'Connectable',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from .user import ClientUser
|
||||
|
||||
|
||||
class _Undefined:
|
||||
def __repr__(self):
|
||||
return 'see-below'
|
||||
|
||||
|
||||
_undefined = _Undefined()
|
||||
|
||||
class Snowflake(metaclass=abc.ABCMeta):
|
||||
|
||||
@runtime_checkable
|
||||
class Snowflake(Protocol):
|
||||
"""An ABC that details the common operations on a Discord model.
|
||||
|
||||
Almost all :ref:`Discord models <discord_api_models>` meet this
|
||||
@ -61,27 +80,16 @@ class Snowflake(metaclass=abc.ABCMeta):
|
||||
The model's unique ID.
|
||||
"""
|
||||
__slots__ = ()
|
||||
id: int
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the model's creation time as a naive datetime in UTC."""
|
||||
def created_at(self) -> datetime:
|
||||
""":class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is Snowflake:
|
||||
mro = C.__mro__
|
||||
for attr in ('created_at', 'id'):
|
||||
for base in mro:
|
||||
if attr in base.__dict__:
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
return NotImplemented
|
||||
|
||||
class User(metaclass=abc.ABCMeta):
|
||||
@runtime_checkable
|
||||
class User(Snowflake, Protocol):
|
||||
"""An ABC that details the common operations on a Discord user.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -105,35 +113,24 @@ class User(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
name: str
|
||||
discriminator: str
|
||||
avatar: Optional[str]
|
||||
bot: bool
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def display_name(self):
|
||||
def display_name(self) -> str:
|
||||
""":class:`str`: Returns the user's display name."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mention(self):
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is User:
|
||||
if Snowflake.__subclasshook__(C) is NotImplemented:
|
||||
return NotImplemented
|
||||
|
||||
mro = C.__mro__
|
||||
for attr in ('display_name', 'mention', 'name', 'avatar', 'discriminator', 'bot'):
|
||||
for base in mro:
|
||||
if attr in base.__dict__:
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
return NotImplemented
|
||||
|
||||
class PrivateChannel(metaclass=abc.ABCMeta):
|
||||
@runtime_checkable
|
||||
class PrivateChannel(Snowflake, Protocol):
|
||||
"""An ABC that details the common operations on a private Discord channel.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -150,37 +147,28 @@ class PrivateChannel(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is PrivateChannel:
|
||||
if Snowflake.__subclasshook__(C) is NotImplemented:
|
||||
return NotImplemented
|
||||
me: ClientUser
|
||||
|
||||
mro = C.__mro__
|
||||
for base in mro:
|
||||
if 'me' in base.__dict__:
|
||||
return True
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
class _Overwrites:
|
||||
__slots__ = ('id', 'allow', 'deny', 'type')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.allow = kwargs.pop('allow', 0)
|
||||
self.deny = kwargs.pop('deny', 0)
|
||||
self.allow = int(kwargs.pop('allow_new', 0))
|
||||
self.deny = int(kwargs.pop('deny_new', 0))
|
||||
self.type = sys.intern(kwargs.pop('type'))
|
||||
|
||||
def _asdict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'allow': self.allow,
|
||||
'deny': self.deny,
|
||||
'allow': str(self.allow),
|
||||
'deny': str(self.deny),
|
||||
'type': self.type,
|
||||
}
|
||||
|
||||
class GuildChannel:
|
||||
|
||||
class GuildChannel(Protocol):
|
||||
"""An ABC that details the common operations on a Discord guild channel.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -191,6 +179,11 @@ class GuildChannel:
|
||||
|
||||
This ABC must also implement :class:`~discord.abc.Snowflake`.
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -259,6 +252,13 @@ class GuildChannel:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
rtc_region = options.pop('rtc_region')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
||||
|
||||
lock_permissions = options.pop('sync_permissions', False)
|
||||
|
||||
try:
|
||||
@ -356,7 +356,7 @@ class GuildChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -710,6 +710,126 @@ class GuildChannel:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def move(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
A rich interface to help move a channel relative to other channels.
|
||||
|
||||
If exact position movement is required, :meth:`edit` should be used instead.
|
||||
|
||||
You must have the :attr:`~discord.Permissions.manage_channels` permission to
|
||||
do this.
|
||||
|
||||
.. note::
|
||||
|
||||
Voice channels will always be sorted below text channels.
|
||||
This is a Discord limitation.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
------------
|
||||
beginning: :class:`bool`
|
||||
Whether to move the channel to the beginning of the
|
||||
channel list (or category if given).
|
||||
This is mutually exclusive with ``end``, ``before``, and ``after``.
|
||||
end: :class:`bool`
|
||||
Whether to move the channel to the end of the
|
||||
channel list (or category if given).
|
||||
This is mutually exclusive with ``beginning``, ``before``, and ``after``.
|
||||
before: :class:`abc.Snowflake`
|
||||
The channel that should be before our current channel.
|
||||
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
|
||||
after: :class:`abc.Snowflake`
|
||||
The channel that should be after our current channel.
|
||||
This is mutually exclusive with ``beginning``, ``end``, and ``before``.
|
||||
offset: :class:`int`
|
||||
The number of channels to offset the move by. For example,
|
||||
an offset of ``2`` with ``beginning=True`` would move
|
||||
it 2 after the beginning. A positive number moves it below
|
||||
while a negative number moves it above. Note that this
|
||||
number is relative and computed after the ``beginning``,
|
||||
``end``, ``before``, and ``after`` parameters.
|
||||
category: Optional[:class:`abc.Snowflake`]
|
||||
The category to move this channel under.
|
||||
If ``None`` is given then it moves it out of the category.
|
||||
This parameter is ignored if moving a category channel.
|
||||
sync_permissions: :class:`bool`
|
||||
Whether to sync the permissions with the category (if given).
|
||||
reason: :class:`str`
|
||||
The reason for the move.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
An invalid position was given or a bad mix of arguments were passed.
|
||||
Forbidden
|
||||
You do not have permissions to move the channel.
|
||||
HTTPException
|
||||
Moving the channel failed.
|
||||
"""
|
||||
|
||||
if not kwargs:
|
||||
return
|
||||
|
||||
beginning, end = kwargs.get('beginning'), kwargs.get('end')
|
||||
before, after = kwargs.get('before'), kwargs.get('after')
|
||||
offset = kwargs.get('offset', 0)
|
||||
if sum(bool(a) for a in (beginning, end, before, after)) > 1:
|
||||
raise InvalidArgument('Only one of [before, after, end, beginning] can be used.')
|
||||
|
||||
bucket = self._sorting_bucket
|
||||
parent_id = kwargs.get('category', ...)
|
||||
if parent_id not in (..., None):
|
||||
parent_id = parent_id.id
|
||||
channels = [
|
||||
ch
|
||||
for ch in self.guild.channels
|
||||
if ch._sorting_bucket == bucket
|
||||
and ch.category_id == parent_id
|
||||
]
|
||||
else:
|
||||
channels = [
|
||||
ch
|
||||
for ch in self.guild.channels
|
||||
if ch._sorting_bucket == bucket
|
||||
and ch.category_id == self.category_id
|
||||
]
|
||||
|
||||
channels.sort(key=lambda c: (c.position, c.id))
|
||||
|
||||
try:
|
||||
# Try to remove ourselves from the channel list
|
||||
channels.remove(self)
|
||||
except ValueError:
|
||||
# If we're not there then it's probably due to not being in the category
|
||||
pass
|
||||
|
||||
index = None
|
||||
if beginning:
|
||||
index = 0
|
||||
elif end:
|
||||
index = len(channels)
|
||||
elif before:
|
||||
index = next((i for i, c in enumerate(channels) if c.id == before.id), None)
|
||||
elif after:
|
||||
index = next((i + 1 for i, c in enumerate(channels) if c.id == after.id), None)
|
||||
|
||||
if index is None:
|
||||
raise InvalidArgument('Could not resolve appropriate move position')
|
||||
|
||||
channels.insert(max((index + offset), 0), self)
|
||||
payload = []
|
||||
lock_permissions = kwargs.get('sync_permissions', False)
|
||||
reason = kwargs.get('reason')
|
||||
for index, channel in enumerate(channels):
|
||||
d = {'id': channel.id, 'position': index}
|
||||
if parent_id is not ... and channel.id == self.id:
|
||||
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
|
||||
payload.append(d)
|
||||
|
||||
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
|
||||
|
||||
async def create_invite(self, *, reason=None, **fields):
|
||||
"""|coro|
|
||||
|
||||
@ -784,7 +904,8 @@ class GuildChannel:
|
||||
|
||||
return result
|
||||
|
||||
class Messageable(metaclass=abc.ABCMeta):
|
||||
|
||||
class Messageable(Protocol):
|
||||
"""An ABC that details the common operations on a model that can send messages.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -795,11 +916,16 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
- :class:`~discord.User`
|
||||
- :class:`~discord.Member`
|
||||
- :class:`~discord.ext.commands.Context`
|
||||
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _get_channel(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -904,7 +1030,7 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
allowed_mentions = state.allowed_mentions and state.allowed_mentions.to_dict()
|
||||
|
||||
if mention_author is not None:
|
||||
allowed_mentions = allowed_mentions or {}
|
||||
allowed_mentions = allowed_mentions or AllowedMentions().to_dict()
|
||||
allowed_mentions['replied_user'] = bool(mention_author)
|
||||
|
||||
if reference is not None:
|
||||
@ -942,8 +1068,8 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
f.close()
|
||||
else:
|
||||
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
|
||||
nonce=nonce, allowed_mentions=allowed_mentions,
|
||||
message_reference=reference)
|
||||
nonce=nonce, allowed_mentions=allowed_mentions,
|
||||
message_reference=reference)
|
||||
|
||||
ret = state.create_message(channel=channel, data=data)
|
||||
if delete_after is not None:
|
||||
@ -1068,13 +1194,16 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
that this would make it a slow operation.
|
||||
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages before this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages after this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages around this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
When using this argument, the maximum limit is 101. Note that if the limit is an
|
||||
even number then this will return at most limit + 1 messages.
|
||||
oldest_first: Optional[:class:`bool`]
|
||||
@ -1095,21 +1224,25 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
|
||||
|
||||
class Connectable(metaclass=abc.ABCMeta):
|
||||
|
||||
class Connectable(Protocol):
|
||||
"""An ABC that details the common operations on a channel that can
|
||||
connect to a voice server.
|
||||
|
||||
The following implement this ABC:
|
||||
|
||||
- :class:`~discord.VoiceChannel`
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_voice_client_key(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_voice_state_pair(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -1146,9 +1279,6 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
A voice client that is fully connected to the voice server.
|
||||
"""
|
||||
|
||||
if not issubclass(cls, VoiceProtocol):
|
||||
raise TypeError('Type must meet VoiceProtocol abstract base class.')
|
||||
|
||||
key_id, _ = self._get_voice_client_key()
|
||||
state = self._state
|
||||
|
||||
@ -1157,6 +1287,10 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
|
||||
client = state._get_client()
|
||||
voice = cls(client, self)
|
||||
|
||||
if not isinstance(voice, VoiceProtocol):
|
||||
raise TypeError('Type must meet VoiceProtocol abstract base class.')
|
||||
|
||||
state._add_voice_client(key_id, voice)
|
||||
|
||||
try:
|
||||
@ -1167,6 +1301,6 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
except Exception:
|
||||
# we don't care if disconnect failed because connection failed
|
||||
pass
|
||||
raise # re-raise
|
||||
raise # re-raise
|
||||
|
||||
return voice
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -187,7 +185,10 @@ class Activity(BaseActivity):
|
||||
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))
|
||||
|
||||
activity_type = kwargs.pop('type', -1)
|
||||
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
|
||||
|
||||
emoji = kwargs.pop('emoji', None)
|
||||
if emoji is not None:
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
@ -196,16 +197,16 @@ class Activity(BaseActivity):
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
'type',
|
||||
'name',
|
||||
'url',
|
||||
'details',
|
||||
'application_id',
|
||||
'session_id',
|
||||
'emoji',
|
||||
('type', self.type),
|
||||
('name', self.name),
|
||||
('url', self.url),
|
||||
('details', self.details),
|
||||
('application_id', self.application_id),
|
||||
('session_id', self.session_id),
|
||||
('emoji', self.emoji),
|
||||
)
|
||||
mapped = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in attrs)
|
||||
return '<Activity %s>' % mapped
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<Activity {inner}>'
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
@ -227,17 +228,21 @@ class Activity(BaseActivity):
|
||||
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)
|
||||
timestamp = self.timestamps['start'] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
@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)
|
||||
timestamp = self.timestamps['end'] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def large_image_url(self):
|
||||
@ -250,7 +255,7 @@ class Activity(BaseActivity):
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, large_image)
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{large_image}.png'
|
||||
|
||||
@property
|
||||
def small_image_url(self):
|
||||
@ -263,7 +268,7 @@ class Activity(BaseActivity):
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, small_image)
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
|
||||
@property
|
||||
def large_image_text(self):
|
||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
||||
@ -302,10 +307,6 @@ class Game(BaseActivity):
|
||||
-----------
|
||||
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
|
||||
-----------
|
||||
@ -322,20 +323,12 @@ class Game(BaseActivity):
|
||||
try:
|
||||
timestamps = extra['timestamps']
|
||||
except KeyError:
|
||||
self._extract_timestamp(extra, 'start')
|
||||
self._extract_timestamp(extra, 'end')
|
||||
self._start = 0
|
||||
self._end = 0
|
||||
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`.
|
||||
@ -348,21 +341,21 @@ class Game(BaseActivity):
|
||||
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 datetime.datetime.utcfromtimestamp(self._start / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
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 datetime.datetime.utcfromtimestamp(self._end / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Game name={0.name!r}>'.format(self)
|
||||
return f'<Game name={self.name!r}>'
|
||||
|
||||
def to_dict(self):
|
||||
timestamps = {}
|
||||
@ -455,7 +448,7 @@ class Streaming(BaseActivity):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Streaming name={0.name!r}>'.format(self)
|
||||
return f'<Streaming name={self.name!r}>'
|
||||
|
||||
@property
|
||||
def twitch_name(self):
|
||||
@ -693,8 +686,14 @@ class CustomActivity(BaseActivity):
|
||||
|
||||
if emoji is None:
|
||||
self.emoji = emoji
|
||||
else:
|
||||
elif isinstance(emoji, dict):
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
elif isinstance(emoji, str):
|
||||
self.emoji = PartialEmoji(name=emoji)
|
||||
elif isinstance(emoji, PartialEmoji):
|
||||
self.emoji = emoji
|
||||
else:
|
||||
raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
@ -733,7 +732,7 @@ class CustomActivity(BaseActivity):
|
||||
def __str__(self):
|
||||
if self.emoji:
|
||||
if self.name:
|
||||
return '%s %s' % (self.emoji, self.name)
|
||||
return f'{self.emoji} {self.name}'
|
||||
return str(self.emoji)
|
||||
else:
|
||||
return str(self.name)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,9 @@ from .user import User
|
||||
from .asset import Asset
|
||||
from .team import Team
|
||||
|
||||
__all__ = (
|
||||
'AppInfo',
|
||||
)
|
||||
|
||||
class AppInfo:
|
||||
"""Represents the application info for the bot provided by Discord.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,10 @@ from .errors import DiscordException
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'Asset',
|
||||
)
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
@ -74,11 +76,11 @@ class Asset:
|
||||
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))
|
||||
raise InvalidArgument(f"format must be None or one of {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))
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if user.avatar is None:
|
||||
return user.default_avatar_url
|
||||
@ -96,7 +98,7 @@ class Asset:
|
||||
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))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
|
||||
return cls(state, url)
|
||||
@ -109,7 +111,7 @@ class Asset:
|
||||
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))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
|
||||
return cls(state, url)
|
||||
@ -119,7 +121,7 @@ class Asset:
|
||||
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))
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if hash is None:
|
||||
return cls(state)
|
||||
@ -132,11 +134,11 @@ class Asset:
|
||||
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))
|
||||
raise InvalidArgument(f"format must be one of {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))
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if guild.icon is None:
|
||||
return cls(state)
|
||||
@ -156,16 +158,16 @@ class Asset:
|
||||
@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))
|
||||
raise InvalidArgument(f"format must be None or one of {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))
|
||||
raise InvalidArgument(f"static_format must be one of {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))
|
||||
|
||||
return cls(state, f'/emojis/{emoji.id}.{format}')
|
||||
|
||||
def __str__(self):
|
||||
return self.BASE + self._url if self._url is not None else ''
|
||||
|
||||
@ -178,7 +180,7 @@ class Asset:
|
||||
return self._url is not None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Asset url={0._url!r}>'.format(self)
|
||||
return f'<Asset url={self._url!r}>'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,13 @@ from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
|
||||
__all__ = (
|
||||
'AuditLogDiff',
|
||||
'AuditLogChanges',
|
||||
'AuditLogEntry',
|
||||
)
|
||||
|
||||
def _transform_verification_level(entry, data):
|
||||
return enums.try_enum(enums.VerificationLevel, data)
|
||||
@ -51,8 +56,7 @@ def _transform_snowflake(entry, data):
|
||||
def _transform_channel(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
channel = entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
return channel
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
def _transform_owner_id(entry, data):
|
||||
if data is None:
|
||||
@ -94,7 +98,7 @@ class AuditLogDiff:
|
||||
|
||||
def __repr__(self):
|
||||
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
||||
return '<AuditLogDiff %s>' % values
|
||||
return f'<AuditLogDiff {values}>'
|
||||
|
||||
class AuditLogChanges:
|
||||
TRANSFORMERS = {
|
||||
@ -166,7 +170,7 @@ class AuditLogChanges:
|
||||
self.before.color = self.before.colour
|
||||
|
||||
def __repr__(self):
|
||||
return '<AuditLogChanges before=%r after=%r>' % (self.before, self.after)
|
||||
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
|
||||
|
||||
def _handle_role(self, first, second, entry, elem):
|
||||
if not hasattr(first, 'roles'):
|
||||
@ -187,11 +191,28 @@ class AuditLogChanges:
|
||||
|
||||
setattr(second, 'roles', data)
|
||||
|
||||
class AuditLogEntry:
|
||||
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`
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
import time
|
||||
import random
|
||||
|
||||
__all__ = (
|
||||
'ExponentialBackoff',
|
||||
)
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""An implementation of the exponential backoff algorithm
|
||||
|
||||
|
155
discord/calls.py
155
discord/calls.py
@ -1,155 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 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`.
|
||||
|
||||
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."""
|
||||
return self.ended_timestamp is not None
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: The private channel associated with this message."""
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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."""
|
||||
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."""
|
||||
return self.call.channel
|
||||
|
||||
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``.
|
||||
|
||||
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)
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,7 +27,7 @@ import asyncio
|
||||
|
||||
import discord.abc
|
||||
from .permissions import Permissions
|
||||
from .enums import ChannelType, try_enum
|
||||
from .enums import ChannelType, try_enum, VoiceRegion
|
||||
from .mixins import Hashable
|
||||
from . import utils
|
||||
from .asset import Asset
|
||||
@ -38,6 +36,7 @@ from .errors import ClientException, NoMoreItems, InvalidArgument
|
||||
__all__ = (
|
||||
'TextChannel',
|
||||
'VoiceChannel',
|
||||
'StageChannel',
|
||||
'DMChannel',
|
||||
'CategoryChannel',
|
||||
'StoreChannel',
|
||||
@ -114,7 +113,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
('news', self.is_news()),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
@ -148,6 +148,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
.. versionadded:: 1.5.0.2"""
|
||||
return self.permissions_for(self.guild.me).send_messages
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
@ -156,8 +157,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
||||
@ -247,6 +246,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
"""
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'topic': self.topic,
|
||||
@ -254,8 +254,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
'rate_limit_per_user': self.slowmode_delay
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def delete_messages(self, messages):
|
||||
"""|coro|
|
||||
|
||||
@ -320,10 +318,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
account). The :attr:`~Permissions.read_message_history` permission is
|
||||
also needed to retrieve message history.
|
||||
|
||||
Internally, this employs a different number of strategies depending
|
||||
on the conditions met such as if a bulk delete is possible or if
|
||||
the account is a user bot or not.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
@ -354,8 +348,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
bulk: :class:`bool`
|
||||
If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting
|
||||
a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will
|
||||
fall back to single delete if current account is a user bot, or if messages are
|
||||
older than two weeks.
|
||||
fall back to single delete if messages are older than two weeks.
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -378,7 +371,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
count = 0
|
||||
|
||||
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
|
||||
strategy = self.delete_messages if self._state.is_bot and bulk else _single_delete_strategy
|
||||
strategy = self.delete_messages if bulk else _single_delete_strategy
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -546,7 +539,80 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
from .message import PartialMessage
|
||||
return PartialMessage(channel=self, id=message_id)
|
||||
|
||||
class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
||||
'_state', 'position', '_overwrites', 'category_id',
|
||||
'rtc_region')
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self._update(guild, data)
|
||||
|
||||
def _get_voice_client_key(self):
|
||||
return self.guild.id, 'guild_id'
|
||||
|
||||
def _get_voice_state_pair(self):
|
||||
return self.guild.id, self.id
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data['name']
|
||||
self.rtc_region = data.get('rtc_region')
|
||||
if self.rtc_region:
|
||||
self.rtc_region = try_enum(VoiceRegion, self.rtc_region)
|
||||
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
||||
self.position = data['position']
|
||||
self.bitrate = data.get('bitrate')
|
||||
self.user_limit = data.get('user_limit')
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
def _sorting_bucket(self):
|
||||
return ChannelType.voice.value
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
||||
ret = []
|
||||
for user_id, state in self.guild._voice_states.items():
|
||||
if state.channel and state.channel.id == self.id:
|
||||
member = self.guild.get_member(user_id)
|
||||
if member is not None:
|
||||
ret.append(member)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def voice_states(self):
|
||||
"""Returns a mapping of member IDs who have voice states in this channel.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
.. note::
|
||||
|
||||
This function is intentionally low level to replace :attr:`members`
|
||||
when the member cache is unavailable.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Mapping[:class:`int`, :class:`VoiceState`]
|
||||
The mapping of member ID to a voice state.
|
||||
"""
|
||||
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# voice channels cannot be edited by people who can't connect to them
|
||||
# It also implicitly denies all other voice perms
|
||||
if not base.connect:
|
||||
denied = Permissions.voice()
|
||||
denied.update(manage_channels=True, manage_roles=True)
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
class VoiceChannel(VocalGuildChannel):
|
||||
"""Represents a Discord guild voice channel.
|
||||
|
||||
.. container:: operations
|
||||
@ -584,101 +650,40 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a voice channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
||||
'_state', 'position', '_overwrites', 'category_id')
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self._update(guild, data)
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
attrs = [
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('rtc_region', self.rtc_region),
|
||||
('position', self.position),
|
||||
('bitrate', self.bitrate),
|
||||
('user_limit', self.user_limit),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
|
||||
|
||||
def _get_voice_client_key(self):
|
||||
return self.guild.id, 'guild_id'
|
||||
|
||||
def _get_voice_state_pair(self):
|
||||
return self.guild.id, self.id
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.voice
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data['name']
|
||||
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
||||
self.position = data['position']
|
||||
self.bitrate = data.get('bitrate')
|
||||
self.user_limit = data.get('user_limit')
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
def _sorting_bucket(self):
|
||||
return ChannelType.voice.value
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
||||
ret = []
|
||||
for user_id, state in self.guild._voice_states.items():
|
||||
if state.channel.id == self.id:
|
||||
member = self.guild.get_member(user_id)
|
||||
if member is not None:
|
||||
ret.append(member)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def voice_states(self):
|
||||
"""Returns a mapping of member IDs who have voice states in this channel.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
.. note::
|
||||
|
||||
This function is intentionally low level to replace :attr:`members`
|
||||
when the member cache is unavailable.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Mapping[:class:`int`, :class:`VoiceState`]
|
||||
The mapping of member ID to a voice state.
|
||||
"""
|
||||
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
|
||||
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# voice channels cannot be edited by people who can't connect to them
|
||||
# It also implicitly denies all other voice perms
|
||||
if not base.connect:
|
||||
denied = Permissions.voice()
|
||||
denied.update(manage_channels=True, manage_roles=True)
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'bitrate': self.bitrate,
|
||||
'user_limit': self.user_limit
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -711,6 +716,136 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
overwrites: :class:`dict`
|
||||
A :class:`dict` of target (either a role or a member) to
|
||||
:class:`PermissionOverwrite` to apply to the channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The new region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If the permission overwrite information is not in proper form.
|
||||
Forbidden
|
||||
You do not have permissions to edit the channel.
|
||||
HTTPException
|
||||
Editing the channel failed.
|
||||
"""
|
||||
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
class StageChannel(VocalGuildChannel):
|
||||
"""Represents a Discord guild stage channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the channel's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The channel name.
|
||||
guild: :class:`Guild`
|
||||
The guild the channel belongs to.
|
||||
id: :class:`int`
|
||||
The channel ID.
|
||||
topic: Optional[:class:`str`]
|
||||
The channel's topic. ``None`` if it isn't set.
|
||||
category_id: Optional[:class:`int`]
|
||||
The category channel ID this channel belongs to, if applicable.
|
||||
position: :class:`int`
|
||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||
top channel is position 0.
|
||||
bitrate: :class:`int`
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a stage channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the stage channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
"""
|
||||
__slots__ = ('topic',)
|
||||
|
||||
def __repr__(self):
|
||||
attrs = [
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('topic', self.topic),
|
||||
('rtc_region', self.rtc_region),
|
||||
('position', self.position),
|
||||
('bitrate', self.bitrate),
|
||||
('user_limit', self.user_limit),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
def _update(self, guild, data):
|
||||
super()._update(guild, data)
|
||||
self.topic = data.get('topic')
|
||||
|
||||
@property
|
||||
def requesting_to_speak(self):
|
||||
"""List[:class:`Member`]: A list of members who are requesting to speak in the stage channel."""
|
||||
return [member for member in self.members if member.voice.requested_to_speak_at is not None]
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.stage_voice
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'topic': self.topic,
|
||||
}, name=name, reason=reason)
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
Edits the channel.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: :class:`str`
|
||||
The new channel's name.
|
||||
topic: :class:`str`
|
||||
The new channel's topic.
|
||||
position: :class:`int`
|
||||
The new channel's position.
|
||||
sync_permissions: :class:`bool`
|
||||
Whether to sync permissions with the channel's new or pre-existing
|
||||
category. Defaults to ``False``.
|
||||
category: Optional[:class:`CategoryChannel`]
|
||||
The new category for this channel. Can be ``None`` to remove the
|
||||
category.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this channel. Shows up on the audit log.
|
||||
overwrites: :class:`dict`
|
||||
A :class:`dict` of target (either a role or a member) to
|
||||
:class:`PermissionOverwrite` to apply to the channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The new region for the stage channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -791,13 +926,12 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
""":class:`bool`: Checks if the category is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'nsfw': self.nsfw
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -835,6 +969,11 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
|
||||
await self._edit(options=options, reason=reason)
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.move)
|
||||
async def move(self, **kwargs):
|
||||
kwargs.pop('category', None)
|
||||
await super().move(**kwargs)
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
"""List[:class:`abc.GuildChannel`]: Returns the channels that are under this category.
|
||||
@ -866,6 +1005,18 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
ret.sort(key=lambda c: (c.position, c.id))
|
||||
return ret
|
||||
|
||||
@property
|
||||
def stage_channels(self):
|
||||
"""List[:class:`StageChannel`]: Returns the voice channels that are under this category.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
ret = [c for c in self.guild.channels
|
||||
if c.category_id == self.id
|
||||
and isinstance(c, StageChannel)]
|
||||
ret.sort(key=lambda c: (c.position, c.id))
|
||||
return ret
|
||||
|
||||
async def create_text_channel(self, name, *, overwrites=None, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -890,6 +1041,20 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""
|
||||
return await self.guild.create_voice_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
|
||||
|
||||
async def create_stage_channel(self, name, *, overwrites=None, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`StageChannel`
|
||||
The channel that was just created.
|
||||
"""
|
||||
return await self.guild.create_stage_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
|
||||
|
||||
class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord guild store channel.
|
||||
|
||||
@ -953,6 +1118,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.store
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
@ -961,19 +1127,16 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
def is_nsfw(self):
|
||||
""":class:`bool`: Checks if the channel is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'nsfw': self.nsfw
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -1059,7 +1222,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return 'Direct Message with %s' % self.recipient
|
||||
return f'Direct Message with {self.recipient}'
|
||||
|
||||
def __repr__(self):
|
||||
return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self)
|
||||
@ -1255,8 +1418,8 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
This returns all the Text related permissions set to ``True`` except:
|
||||
|
||||
- send_tts_messages: You cannot send TTS messages in a DM.
|
||||
- manage_messages: You cannot delete others messages in a DM.
|
||||
- :attr:`~Permissions.send_tts_messages`: You cannot send TTS messages in a DM.
|
||||
- :attr:`~Permissions.manage_messages`: You cannot delete others messages in a DM.
|
||||
|
||||
This also checks the kick_members permission if the user is the owner.
|
||||
|
||||
@ -1281,86 +1444,6 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
return base
|
||||
|
||||
async def add_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Adds recipients to this group.
|
||||
|
||||
A group can only have a maximum of 10 members.
|
||||
Attempting to add more ends up in an exception. To
|
||||
add a recipient to the group, you must have a relationship
|
||||
with the user of type :attr:`RelationshipType.friend`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to add to this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Adding a recipient to this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.add_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def remove_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Removes recipients from this group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to remove from this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Removing a recipient from this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.remove_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The new name to change the group to.
|
||||
Could be ``None`` to remove the name.
|
||||
icon: Optional[:class:`bytes`]
|
||||
A :term:`py:bytes-like object` representing the new icon.
|
||||
Could be ``None`` to remove the icon.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the group failed.
|
||||
"""
|
||||
|
||||
try:
|
||||
icon_bytes = fields['icon']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if icon_bytes is not None:
|
||||
fields['icon'] = utils._bytes_to_base64_data(icon_bytes)
|
||||
|
||||
data = await self._state.http.edit_group(self.id, **fields)
|
||||
self._update_group(data)
|
||||
|
||||
async def leave(self):
|
||||
"""|coro|
|
||||
|
||||
@ -1392,5 +1475,7 @@ def _channel_factory(channel_type):
|
||||
return TextChannel, value
|
||||
elif value is ChannelType.store:
|
||||
return StoreChannel, value
|
||||
elif value is ChannelType.stage_voice:
|
||||
return StageChannel, value
|
||||
else:
|
||||
return None, value
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -34,7 +32,7 @@ import re
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .user import User, Profile
|
||||
from .user import User
|
||||
from .invite import Invite
|
||||
from .template import Template
|
||||
from .widget import Widget
|
||||
@ -57,16 +55,14 @@ from .iterators import GuildIterator
|
||||
from .appinfo import AppInfo
|
||||
from .colour import Color, Colour
|
||||
|
||||
__all__ = (
|
||||
'Client',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def _cancel_tasks(loop):
|
||||
try:
|
||||
task_retriever = asyncio.Task.all_tasks
|
||||
except AttributeError:
|
||||
# future proofing for 3.9 I guess
|
||||
task_retriever = asyncio.all_tasks
|
||||
|
||||
tasks = {t for t in task_retriever(loop=loop) if not t.done()}
|
||||
tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()}
|
||||
|
||||
if not tasks:
|
||||
return
|
||||
@ -91,28 +87,11 @@ def _cancel_tasks(loop):
|
||||
def _cleanup_loop(loop):
|
||||
try:
|
||||
_cancel_tasks(loop)
|
||||
if sys.version_info >= (3, 6):
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
finally:
|
||||
log.info('Closing the event loop.')
|
||||
loop.close()
|
||||
|
||||
class _ClientEventTask(asyncio.Task):
|
||||
def __init__(self, original_coro, event_name, coro, *, loop):
|
||||
super().__init__(coro, loop=loop)
|
||||
self.__event_name = event_name
|
||||
self.__original_coro = original_coro
|
||||
|
||||
def __repr__(self):
|
||||
info = [
|
||||
('state', self._state.lower()),
|
||||
('event', self.__event_name),
|
||||
('coro', repr(self.__original_coro)),
|
||||
]
|
||||
if self._exception is not None:
|
||||
info.append(('exception', repr(self._exception)))
|
||||
return '<ClientEventTask {}>'.format(' '.join('%s=%s' % t for t in info))
|
||||
|
||||
class Client:
|
||||
r"""Represents a client connection that connects to Discord.
|
||||
This class is used to interact with the Discord WebSocket and API.
|
||||
@ -141,6 +120,8 @@ class Client:
|
||||
Integer starting at ``0`` and less than :attr:`.shard_count`.
|
||||
shard_count: Optional[:class:`int`]
|
||||
The total number of shards.
|
||||
application_id: :class:`int`
|
||||
The client's application ID.
|
||||
intents: :class:`Intents`
|
||||
The intents that you want to enable for the session. This is a way of
|
||||
disabling and enabling certain gateway events from triggering and being sent.
|
||||
@ -344,10 +325,7 @@ class Client:
|
||||
|
||||
def _get_state(self, **options):
|
||||
return ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
|
||||
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
|
||||
|
||||
async def _syncer(self, guilds):
|
||||
await self.ws.request_sync(guilds)
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
|
||||
def _handle_ready(self):
|
||||
self._ready.set()
|
||||
@ -415,6 +393,16 @@ class Client:
|
||||
"""
|
||||
return self._connection.voice_clients
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
"""Optional[:class:`int`]: The client's application ID.
|
||||
|
||||
If this is not passed via ``__init__`` then this is retrieved
|
||||
through the gateway when an event contains the data. Usually
|
||||
after :func:`on_connect` is called.
|
||||
"""
|
||||
return self._connection.application_id
|
||||
|
||||
def is_ready(self):
|
||||
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
|
||||
return self._ready.is_set()
|
||||
@ -433,7 +421,7 @@ class Client:
|
||||
def _schedule_event(self, coro, event_name, *args, **kwargs):
|
||||
wrapped = self._run_event(coro, event_name, *args, **kwargs)
|
||||
# Schedules the task
|
||||
return _ClientEventTask(original_coro=coro, event_name=event_name, coro=wrapped, loop=self.loop)
|
||||
return asyncio.create_task(wrapped, name=f'discord.py: {event_name}')
|
||||
|
||||
def dispatch(self, event, *args, **kwargs):
|
||||
log.debug('Dispatching event %s', event)
|
||||
@ -484,43 +472,9 @@ class Client:
|
||||
overridden to have a different implementation.
|
||||
Check :func:`~discord.on_error` for more details.
|
||||
"""
|
||||
print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
|
||||
print(f'Ignoring exception in {event_method}', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
@utils.deprecated('Guild.chunk')
|
||||
async def request_offline_members(self, *guilds):
|
||||
r"""|coro|
|
||||
|
||||
Requests previously offline members from the guild to be filled up
|
||||
into the :attr:`.Guild.members` cache. This function is usually not
|
||||
called. It should only be used if you have the ``fetch_offline_members``
|
||||
parameter set to ``False``.
|
||||
|
||||
When the client logs on and connects to the websocket, Discord does
|
||||
not provide the library with offline members if the number of members
|
||||
in the guild is larger than 250. You can check if a guild is large
|
||||
if :attr:`.Guild.large` is ``True``.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is deprecated. Use :meth:`Guild.chunk` instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*guilds: :class:`.Guild`
|
||||
An argument list of guilds to request offline members for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.InvalidArgument`
|
||||
If any guild is unavailable in the collection.
|
||||
"""
|
||||
if any(g.unavailable for g in guilds):
|
||||
raise InvalidArgument('An unavailable guild was passed.')
|
||||
|
||||
for guild in guilds:
|
||||
await self._connection.chunk_guild(guild)
|
||||
|
||||
# hooks
|
||||
|
||||
async def _call_before_identify_hook(self, shard_id, *, initial=False):
|
||||
@ -553,7 +507,7 @@ class Client:
|
||||
|
||||
# login state management
|
||||
|
||||
async def login(self, token, *, bot=True):
|
||||
async def login(self, token):
|
||||
"""|coro|
|
||||
|
||||
Logs in the client with the specified credentials.
|
||||
@ -572,9 +526,6 @@ class Client:
|
||||
token: :class:`str`
|
||||
The authentication token. Do not prefix this token with
|
||||
anything as the library will do it for you.
|
||||
bot: :class:`bool`
|
||||
Keyword argument that specifies if the account logging on is a bot
|
||||
token or not.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -587,21 +538,7 @@ class Client:
|
||||
"""
|
||||
|
||||
log.info('logging in using static token')
|
||||
await self.http.static_login(token.strip(), bot=bot)
|
||||
self._connection.is_bot = bot
|
||||
|
||||
async def logout(self):
|
||||
"""|coro|
|
||||
|
||||
Logs out of Discord and closes all connections.
|
||||
|
||||
.. note::
|
||||
|
||||
This is just an alias to :meth:`close`. If you want
|
||||
to do extraneous cleanup when subclassing, it is suggested
|
||||
to override :meth:`close` instead.
|
||||
"""
|
||||
await self.close()
|
||||
await self.http.static_login(token.strip())
|
||||
|
||||
async def connect(self, *, reconnect=True):
|
||||
"""|coro|
|
||||
@ -722,7 +659,7 @@ class Client:
|
||||
self._connection.clear()
|
||||
self.http.recreate()
|
||||
|
||||
async def start(self, *args, **kwargs):
|
||||
async def start(self, token, *, reconnect=True):
|
||||
"""|coro|
|
||||
|
||||
A shorthand coroutine for :meth:`login` + :meth:`connect`.
|
||||
@ -732,13 +669,7 @@ class Client:
|
||||
TypeError
|
||||
An unexpected keyword argument was received.
|
||||
"""
|
||||
bot = kwargs.pop('bot', True)
|
||||
reconnect = kwargs.pop('reconnect', True)
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("unexpected keyword argument(s) %s" % list(kwargs.keys()))
|
||||
|
||||
await self.login(*args, bot=bot)
|
||||
await self.login(token)
|
||||
await self.connect(reconnect=reconnect)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
@ -754,7 +685,7 @@ class Client:
|
||||
try:
|
||||
loop.run_until_complete(start(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
loop.run_until_complete(logout())
|
||||
loop.run_until_complete(close())
|
||||
# cancel all tasks lingering
|
||||
finally:
|
||||
loop.close()
|
||||
@ -833,12 +764,10 @@ class Client:
|
||||
|
||||
@allowed_mentions.setter
|
||||
def allowed_mentions(self, value):
|
||||
if value is None:
|
||||
self._connection.allowed_mentions = value
|
||||
elif isinstance(value, AllowedMentions):
|
||||
if value is None or isinstance(value, AllowedMentions):
|
||||
self._connection.allowed_mentions = value
|
||||
else:
|
||||
raise TypeError('allowed_mentions must be AllowedMentions not {0.__class__!r}'.format(value))
|
||||
raise TypeError(f'allowed_mentions must be AllowedMentions not {value.__class__!r}')
|
||||
|
||||
@property
|
||||
def intents(self):
|
||||
@ -937,8 +866,7 @@ class Client:
|
||||
"""
|
||||
|
||||
for guild in self.guilds:
|
||||
for channel in guild.channels:
|
||||
yield channel
|
||||
yield from guild.channels
|
||||
|
||||
def get_all_members(self):
|
||||
"""Returns a generator with every :class:`.Member` the client can see.
|
||||
@ -955,8 +883,7 @@ class Client:
|
||||
A member the client can see.
|
||||
"""
|
||||
for guild in self.guilds:
|
||||
for member in guild.members:
|
||||
yield member
|
||||
yield from guild.members
|
||||
|
||||
# listeners/waiters
|
||||
|
||||
@ -1154,9 +1081,7 @@ class Client:
|
||||
# Guild stuff
|
||||
|
||||
def fetch_guilds(self, *, limit=100, before=None, after=None):
|
||||
"""|coro|
|
||||
|
||||
Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
||||
"""Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -1191,10 +1116,12 @@ class Client:
|
||||
Defaults to ``100``.
|
||||
before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieves guilds before this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve guilds after this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -1306,15 +1233,13 @@ class Client:
|
||||
if icon is not None:
|
||||
icon = utils._bytes_to_base64_data(icon)
|
||||
|
||||
if region is None:
|
||||
region = VoiceRegion.us_west.value
|
||||
else:
|
||||
region = region.value
|
||||
region = region or VoiceRegion.us_west
|
||||
region_value = region.value
|
||||
|
||||
if code:
|
||||
data = await self.http.create_from_template(code, name, region, icon)
|
||||
data = await self.http.create_from_template(code, name, region_value, icon)
|
||||
else:
|
||||
data = await self.http.create_guild(name, region, icon)
|
||||
data = await self.http.create_guild(name, region_value, icon)
|
||||
return Guild(data=data, state=self._connection)
|
||||
|
||||
# Invite management
|
||||
@ -1502,48 +1427,6 @@ class Client:
|
||||
data = await self.http.get_user(user_id)
|
||||
return User(state=self._connection, data=data)
|
||||
|
||||
async def fetch_user_profile(self, user_id):
|
||||
"""|coro|
|
||||
|
||||
Gets an arbitrary user's profile.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
user_id: :class:`int`
|
||||
The ID of the user to fetch their profile for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.Forbidden`
|
||||
Not allowed to fetch profiles.
|
||||
:exc:`.HTTPException`
|
||||
Fetching the profile failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`.Profile`
|
||||
The profile of the user.
|
||||
"""
|
||||
|
||||
state = self._connection
|
||||
data = await self.http.get_user_profile(user_id)
|
||||
|
||||
def transform(d):
|
||||
return state._get_guild(int(d['id']))
|
||||
|
||||
since = data.get('premium_since')
|
||||
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
|
||||
user = data['user']
|
||||
return Profile(flags=user.get('flags', 0),
|
||||
premium_since=utils.parse_time(since),
|
||||
mutual_guilds=mutual_guilds,
|
||||
user=User(data=user, state=state),
|
||||
connected_accounts=data['connected_accounts'])
|
||||
|
||||
async def fetch_channel(self, channel_id):
|
||||
"""|coro|
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,6 +25,11 @@ DEALINGS IN THE SOFTWARE.
|
||||
import colorsys
|
||||
import random
|
||||
|
||||
__all__ = (
|
||||
'Colour',
|
||||
'Color',
|
||||
)
|
||||
|
||||
class Colour:
|
||||
"""Represents a Discord role colour. This class is similar
|
||||
to a (red, green, blue) :class:`tuple`.
|
||||
@ -61,7 +64,7 @@ class Colour:
|
||||
|
||||
def __init__(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int parameter, received %s instead.' % value.__class__.__name__)
|
||||
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
|
||||
|
||||
self.value = value
|
||||
|
||||
@ -75,10 +78,10 @@ class Colour:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return '#{:0>6x}'.format(self.value)
|
||||
return f'#{self.value:0>6x}'
|
||||
|
||||
def __repr__(self):
|
||||
return '<Colour value=%s>' % self.value
|
||||
return f'<Colour value={self.value}>'
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
@ -119,18 +122,32 @@ class Colour:
|
||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||
|
||||
@classmethod
|
||||
def random(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a random value.
|
||||
|
||||
.. versionadded:: 1.6"""
|
||||
return cls(random.randint(0x000000,0xffffff))
|
||||
|
||||
@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``."""
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import asyncio
|
||||
|
||||
__all__ = (
|
||||
'Typing',
|
||||
)
|
||||
|
||||
def _typing_done_callback(fut):
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,37 +22,88 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
|
||||
|
||||
from . import utils
|
||||
from .colour import Colour
|
||||
|
||||
__all__ = (
|
||||
'Embed',
|
||||
)
|
||||
|
||||
|
||||
class _EmptyEmbed:
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'Embed.Empty'
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return 0
|
||||
|
||||
EmptyEmbed = _EmptyEmbed()
|
||||
|
||||
EmptyEmbed: Final = _EmptyEmbed()
|
||||
|
||||
|
||||
class EmbedProxy:
|
||||
def __init__(self, layer):
|
||||
def __init__(self, layer: Dict[str, Any]):
|
||||
self.__dict__.update(layer)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
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 __repr__(self) -> str:
|
||||
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||
return f'EmbedProxy({inner})'
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> _EmptyEmbed:
|
||||
return EmptyEmbed
|
||||
|
||||
|
||||
E = TypeVar('E', bound='Embed')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from discord.types.embed import Embed as EmbedData, EmbedType
|
||||
|
||||
T = TypeVar('T')
|
||||
MaybeEmpty = Union[T, _EmptyEmbed]
|
||||
|
||||
class _EmbedFooterProxy(Protocol):
|
||||
text: MaybeEmpty[str]
|
||||
icon_url: MaybeEmpty[str]
|
||||
|
||||
class _EmbedFieldProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
value: MaybeEmpty[str]
|
||||
inline: bool
|
||||
|
||||
class _EmbedMediaProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
proxy_url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
class _EmbedVideoProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
class _EmbedProviderProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
|
||||
class _EmbedAuthorProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
icon_url: MaybeEmpty[str]
|
||||
proxy_icon_url: MaybeEmpty[str]
|
||||
|
||||
|
||||
class Embed:
|
||||
"""Represents a Discord embed.
|
||||
|
||||
@ -65,6 +114,12 @@ class Embed:
|
||||
Returns the total size of the embed.
|
||||
Useful for checking if it's within the 6000 character limit.
|
||||
|
||||
.. describe:: bool(b)
|
||||
|
||||
Returns whether the embed has any data set.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
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
|
||||
@ -82,6 +137,8 @@ class Embed:
|
||||
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.
|
||||
@ -89,7 +146,9 @@ class Embed:
|
||||
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.
|
||||
The timestamp of the embed content. This is an aware datetime.
|
||||
If a naive datetime is passed, it is converted to an aware
|
||||
datetime with the local timezone.
|
||||
colour: Union[:class:`Colour`, :class:`int`]
|
||||
The colour code of the embed. Aliased to ``color`` as well.
|
||||
This can be set during initialisation.
|
||||
@ -98,37 +157,64 @@ class Embed:
|
||||
to denote that the value or attribute is empty.
|
||||
"""
|
||||
|
||||
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
|
||||
'_image', '_thumbnail', '_video', '_provider', '_author',
|
||||
'_fields', 'description')
|
||||
__slots__ = (
|
||||
'title',
|
||||
'url',
|
||||
'type',
|
||||
'_timestamp',
|
||||
'_colour',
|
||||
'_footer',
|
||||
'_image',
|
||||
'_thumbnail',
|
||||
'_video',
|
||||
'_provider',
|
||||
'_author',
|
||||
'_fields',
|
||||
'description',
|
||||
)
|
||||
|
||||
Empty = EmptyEmbed
|
||||
Empty: Final = EmptyEmbed
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# swap the colour/color aliases
|
||||
try:
|
||||
colour = kwargs['colour']
|
||||
except KeyError:
|
||||
colour = kwargs.get('color', os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed))
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
title: MaybeEmpty[str] = EmptyEmbed,
|
||||
type: EmbedType = 'rich',
|
||||
url: MaybeEmpty[str] = EmptyEmbed,
|
||||
description: MaybeEmpty[str] = EmptyEmbed,
|
||||
timestamp: datetime.datetime = None,
|
||||
):
|
||||
|
||||
if colour is EmptyEmbed and color is EmptyEmbed:
|
||||
colour = os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed)
|
||||
if isinstance(colour, str):
|
||||
colour = int(colour, 16)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
timestamp = kwargs['timestamp']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
colour = colour if colour is not EmptyEmbed else color
|
||||
self.colour = colour
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.description = description
|
||||
|
||||
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)
|
||||
|
||||
if timestamp:
|
||||
if timestamp.tzinfo is None:
|
||||
timestamp = timestamp.astimezone()
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls: Type[E], data: EmbedData) -> E:
|
||||
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
|
||||
format that Discord expects it to be in.
|
||||
|
||||
@ -144,7 +230,7 @@ class Embed:
|
||||
The dictionary to convert into an embed.
|
||||
"""
|
||||
# we are bypassing __init__ here since it doesn't apply here
|
||||
self = cls.__new__(cls)
|
||||
self: E = cls.__new__(cls)
|
||||
|
||||
# fill in the basic fields
|
||||
|
||||
@ -153,6 +239,15 @@ class Embed:
|
||||
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:
|
||||
@ -175,11 +270,11 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
def copy(self: E) -> E:
|
||||
"""Returns a shallow copy of the embed."""
|
||||
return Embed.from_dict(self.to_dict())
|
||||
return self.__class__.from_dict(self.to_dict())
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
total = len(self.title) + len(self.description)
|
||||
for field in getattr(self, '_fields', []):
|
||||
total += len(field['name']) + len(field['value'])
|
||||
@ -200,43 +295,61 @@ class Embed:
|
||||
|
||||
return total
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return any(
|
||||
(
|
||||
self.title,
|
||||
self.url,
|
||||
self.description,
|
||||
self.colour,
|
||||
self.fields,
|
||||
self.timestamp,
|
||||
self.author,
|
||||
self.thumbnail,
|
||||
self.footer,
|
||||
self.image,
|
||||
self.provider,
|
||||
self.video,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
def colour(self) -> MaybeEmpty[Colour]:
|
||||
return getattr(self, '_colour', EmptyEmbed)
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value):
|
||||
def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
|
||||
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__)
|
||||
raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.')
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
def timestamp(self) -> MaybeEmpty[datetime.datetime]:
|
||||
return getattr(self, '_timestamp', EmptyEmbed)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, value):
|
||||
def timestamp(self, value: MaybeEmpty[datetime.datetime]):
|
||||
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
|
||||
self._timestamp = value
|
||||
else:
|
||||
raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__)
|
||||
raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
|
||||
|
||||
@property
|
||||
def footer(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the footer contents.
|
||||
def footer(self) -> _EmbedFooterProxy:
|
||||
"""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', {}))
|
||||
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
|
||||
|
||||
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
"""Sets the footer for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -260,8 +373,8 @@ class Embed:
|
||||
return self
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the image contents.
|
||||
def image(self) -> _EmbedMediaProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
@ -272,9 +385,9 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_image', {}))
|
||||
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
||||
|
||||
def set_image(self, *, url):
|
||||
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -296,14 +409,14 @@ class Embed:
|
||||
pass
|
||||
else:
|
||||
self._image = {
|
||||
'url': str(url)
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
def thumbnail(self) -> _EmbedMediaProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
@ -314,9 +427,9 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {}))
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||
|
||||
def set_thumbnail(self, *, url):
|
||||
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -338,14 +451,14 @@ class Embed:
|
||||
pass
|
||||
else:
|
||||
self._thumbnail = {
|
||||
'url': str(url)
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the video contents.
|
||||
def video(self) -> _EmbedVideoProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the video contents.
|
||||
|
||||
Possible attributes include:
|
||||
|
||||
@ -355,29 +468,29 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_video', {}))
|
||||
return EmbedProxy(getattr(self, '_video', {})) # type: ignore
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the provider contents.
|
||||
def provider(self) -> _EmbedProviderProxy:
|
||||
"""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', {}))
|
||||
return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the author contents.
|
||||
def author(self) -> _EmbedAuthorProxy:
|
||||
"""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', {}))
|
||||
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
|
||||
|
||||
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
"""Sets the author for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -394,7 +507,7 @@ class Embed:
|
||||
"""
|
||||
|
||||
self._author = {
|
||||
'name': str(name)
|
||||
'name': str(name),
|
||||
}
|
||||
|
||||
if url is not EmptyEmbed:
|
||||
@ -405,7 +518,7 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def remove_author(self):
|
||||
def remove_author(self: E) -> E:
|
||||
"""Clears embed's author information.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -421,16 +534,16 @@ class Embed:
|
||||
return self
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
def fields(self) -> List[_EmbedFieldProxy]:
|
||||
"""Union[List[:class:`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', [])]
|
||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
|
||||
|
||||
def add_field(self, *, name, value, inline=True):
|
||||
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Adds a field to the embed object.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -449,7 +562,7 @@ class Embed:
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
'value': str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@ -459,7 +572,7 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def insert_field_at(self, index, *, name, value, inline=True):
|
||||
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Inserts a field before a specified index to the embed.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -482,7 +595,7 @@ class Embed:
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
'value': str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@ -492,14 +605,14 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def clear_fields(self):
|
||||
def clear_fields(self) -> None:
|
||||
"""Removes all fields from this embed."""
|
||||
try:
|
||||
self._fields.clear()
|
||||
except AttributeError:
|
||||
self._fields = []
|
||||
|
||||
def remove_field(self, index):
|
||||
def remove_field(self, index: int) -> None:
|
||||
"""Removes a field at a specified index.
|
||||
|
||||
If the index is invalid or out of bounds then the error is
|
||||
@ -520,7 +633,7 @@ class Embed:
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
def set_field_at(self, index, *, name, value, inline=True):
|
||||
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Modifies a field to the embed object.
|
||||
|
||||
The index must point to a valid pre-existing field.
|
||||
@ -555,15 +668,17 @@ class Embed:
|
||||
field['inline'] = inline
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> EmbedData:
|
||||
"""Converts this embed object into a dict."""
|
||||
|
||||
# add in the raw data into the dict
|
||||
# fmt: off
|
||||
result = {
|
||||
key[1:]: getattr(self, key)
|
||||
for key in self.__slots__
|
||||
if key[0] == '_' and hasattr(self, key)
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
# deal with basic convenience wrappers
|
||||
|
||||
@ -599,4 +714,4 @@ class Embed:
|
||||
if self.title:
|
||||
result['title'] = self.title
|
||||
|
||||
return result
|
||||
return result # type: ignore
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,10 @@ from . import utils
|
||||
from .partial_emoji import _EmojiTag
|
||||
from .user import User
|
||||
|
||||
__all__ = (
|
||||
'Emoji',
|
||||
)
|
||||
|
||||
class Emoji(_EmojiTag):
|
||||
"""Represents a custom emoji.
|
||||
|
||||
|
120
discord/enums.py
120
discord/enums.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -26,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import types
|
||||
from collections import namedtuple
|
||||
from typing import Any, TYPE_CHECKING, Type, TypeVar
|
||||
|
||||
__all__ = (
|
||||
'Enum',
|
||||
@ -37,18 +36,12 @@ __all__ = (
|
||||
'ContentFilter',
|
||||
'Status',
|
||||
'DefaultAvatar',
|
||||
'RelationshipType',
|
||||
'AuditLogAction',
|
||||
'AuditLogActionCategory',
|
||||
'UserFlags',
|
||||
'ActivityType',
|
||||
'HypeSquadHouse',
|
||||
'NotificationLevel',
|
||||
'PremiumType',
|
||||
'UserContentFilter',
|
||||
'FriendFlags',
|
||||
'TeamMembershipState',
|
||||
'Theme',
|
||||
'WebhookType',
|
||||
'ExpireBehaviour',
|
||||
'ExpireBehavior',
|
||||
@ -57,8 +50,8 @@ __all__ = (
|
||||
|
||||
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)
|
||||
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
|
||||
cls.__str__ = lambda self: f'{name}.{self.name}'
|
||||
return cls
|
||||
|
||||
def _is_descriptor(obj):
|
||||
@ -98,6 +91,7 @@ class EnumMeta(type):
|
||||
attrs['_enum_value_map_'] = value_mapping
|
||||
attrs['_enum_member_map_'] = member_mapping
|
||||
attrs['_enum_member_names_'] = member_names
|
||||
attrs['_enum_value_cls_'] = value_cls
|
||||
actual_cls = super().__new__(cls, name, bases, attrs)
|
||||
value_cls._actual_enum_cls_ = actual_cls
|
||||
return actual_cls
|
||||
@ -112,7 +106,7 @@ class EnumMeta(type):
|
||||
return len(cls._enum_member_names_)
|
||||
|
||||
def __repr__(cls):
|
||||
return '<enum %r>' % cls.__name__
|
||||
return f'<enum {cls.__name__}>'
|
||||
|
||||
@property
|
||||
def __members__(cls):
|
||||
@ -122,7 +116,7 @@ class EnumMeta(type):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
|
||||
raise ValueError(f"{value!r} is not a valid {cls.__name__}")
|
||||
|
||||
def __getitem__(cls, key):
|
||||
return cls._enum_member_map_[key]
|
||||
@ -141,14 +135,16 @@ class EnumMeta(type):
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enum import Enum
|
||||
else:
|
||||
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
|
||||
@ -158,24 +154,30 @@ class ChannelType(Enum):
|
||||
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
|
||||
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'
|
||||
@ -238,22 +240,6 @@ class ContentFilter(Enum):
|
||||
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'
|
||||
@ -276,12 +262,6 @@ class DefaultAvatar(Enum):
|
||||
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
|
||||
@ -423,15 +403,6 @@ class ActivityType(Enum):
|
||||
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
|
||||
@ -451,13 +422,24 @@ class StickerType(Enum):
|
||||
apng = 2
|
||||
lottie = 3
|
||||
|
||||
def try_enum(cls, val):
|
||||
class InteractionType(Enum):
|
||||
ping = 1
|
||||
application_command = 2
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def create_unknown_value(cls: Type[T], val: Any) -> T:
|
||||
value_cls = cls._enum_value_cls_ # type: ignore
|
||||
name = f'unknown_{val}'
|
||||
return value_cls(name=name, value=val)
|
||||
|
||||
def try_enum(cls: Type[T], val: Any) -> T:
|
||||
"""A function that tries to turn the value into enum ``cls``.
|
||||
|
||||
If it fails it returns the value instead.
|
||||
If it fails it returns a proxy invalid value instead.
|
||||
"""
|
||||
|
||||
try:
|
||||
return cls._enum_value_map_[val]
|
||||
return cls._enum_value_map_[val] # type: ignore
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
return val
|
||||
return create_unknown_value(cls, val)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,6 +22,22 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'DiscordException',
|
||||
'ClientException',
|
||||
'NoMoreItems',
|
||||
'GatewayNotFound',
|
||||
'HTTPException',
|
||||
'Forbidden',
|
||||
'NotFound',
|
||||
'DiscordServerError',
|
||||
'InvalidData',
|
||||
'InvalidArgument',
|
||||
'LoginFailure',
|
||||
'ConnectionClosed',
|
||||
'PrivilegedIntentsRequired',
|
||||
)
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for discord.py
|
||||
|
||||
@ -48,7 +62,7 @@ class GatewayNotFound(DiscordException):
|
||||
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)
|
||||
super().__init__(message)
|
||||
|
||||
def flatten_error_dict(d, key=''):
|
||||
items = []
|
||||
@ -104,7 +118,7 @@ class HTTPException(DiscordException):
|
||||
|
||||
fmt = '{0.status} {0.reason} (error code: {1})'
|
||||
if len(self.text):
|
||||
fmt = fmt + ': {2}'
|
||||
fmt += ': {2}'
|
||||
|
||||
super().__init__(fmt.format(self.response, self.code, self.text))
|
||||
|
||||
@ -174,7 +188,7 @@ class ConnectionClosed(ClientException):
|
||||
# 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))
|
||||
super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}')
|
||||
|
||||
class PrivilegedIntentsRequired(ClientException):
|
||||
"""Exception that's thrown when the gateway is requesting privileged intents
|
||||
|
@ -1,17 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
discord.ext.commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2015-2020 Rapptz
|
||||
: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 .bot import *
|
||||
from .context import *
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .help import *
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -42,12 +40,19 @@ from . import errors
|
||||
from .help import HelpCommand, DefaultHelpCommand
|
||||
from .cog import Cog
|
||||
|
||||
__all__ = (
|
||||
'when_mentioned',
|
||||
'when_mentioned_or',
|
||||
'Bot',
|
||||
'AutoShardedBot',
|
||||
)
|
||||
|
||||
def when_mentioned(bot, msg):
|
||||
"""A callable that implements a command prefix equivalent to being mentioned.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
"""
|
||||
return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id]
|
||||
return [f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
|
||||
|
||||
def when_mentioned_or(*prefixes):
|
||||
"""A callable that implements when mentioned or other prefixes provided.
|
||||
@ -110,12 +115,13 @@ class BotBase(GroupMixin):
|
||||
self.description = inspect.cleandoc(description) if description else ''
|
||||
self.owner_id = options.get('owner_id')
|
||||
self.owner_ids = options.get('owner_ids', set())
|
||||
self.strip_after_prefix = options.get('strip_after_prefix', False)
|
||||
|
||||
if self.owner_id and self.owner_ids:
|
||||
raise TypeError('Both owner_id and owner_ids are set.')
|
||||
|
||||
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
||||
raise TypeError('owner_ids must be a collection not {0.__class__!r}'.format(self.owner_ids))
|
||||
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
||||
|
||||
if options.pop('self_bot', False):
|
||||
self._skip_check = lambda x, y: x != y
|
||||
@ -190,11 +196,10 @@ class BotBase(GroupMixin):
|
||||
return
|
||||
|
||||
cog = context.cog
|
||||
if cog:
|
||||
if Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
|
||||
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr)
|
||||
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
# global check registration
|
||||
@ -656,7 +661,13 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.__extensions[key] = lib
|
||||
|
||||
def load_extension(self, name):
|
||||
def _resolve_name(self, name, package):
|
||||
try:
|
||||
return importlib.util.resolve_name(name, package)
|
||||
except ImportError:
|
||||
raise errors.ExtensionNotFound(name)
|
||||
|
||||
def load_extension(self, name, *, package=None):
|
||||
"""Loads an extension.
|
||||
|
||||
An extension is a python module that contains commands, cogs, or
|
||||
@ -672,11 +683,19 @@ class BotBase(GroupMixin):
|
||||
The extension name to load. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when loading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
--------
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionAlreadyLoaded
|
||||
The extension is already loaded.
|
||||
NoEntryPointError
|
||||
@ -685,6 +704,7 @@ class BotBase(GroupMixin):
|
||||
The extension or its setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
if name in self.__extensions:
|
||||
raise errors.ExtensionAlreadyLoaded(name)
|
||||
|
||||
@ -694,7 +714,7 @@ class BotBase(GroupMixin):
|
||||
|
||||
self._load_from_module_spec(spec, name)
|
||||
|
||||
def unload_extension(self, name):
|
||||
def unload_extension(self, name, *, package=None):
|
||||
"""Unloads an extension.
|
||||
|
||||
When the extension is unloaded, all commands, listeners, and cogs are
|
||||
@ -711,13 +731,23 @@ class BotBase(GroupMixin):
|
||||
The extension name to unload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when unloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
ExtensionNotFound
|
||||
The name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionNotLoaded
|
||||
The extension was not loaded.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@ -725,7 +755,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
|
||||
def reload_extension(self, name):
|
||||
def reload_extension(self, name, *, package=None):
|
||||
"""Atomically reloads an extension.
|
||||
|
||||
This replaces the extension with the same extension, only refreshed. This is
|
||||
@ -739,6 +769,12 @@ class BotBase(GroupMixin):
|
||||
The extension name to reload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when reloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -746,12 +782,15 @@ class BotBase(GroupMixin):
|
||||
The extension was not loaded.
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
NoEntryPointError
|
||||
The extension does not have a setup function.
|
||||
ExtensionFailed
|
||||
The extension setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@ -768,7 +807,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
self.load_extension(name)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# if the load failed, the remnants should have been
|
||||
# cleaned from the load_extension function call
|
||||
# so let's load it from our old compiled library.
|
||||
@ -920,6 +959,9 @@ class BotBase(GroupMixin):
|
||||
# Getting here shouldn't happen
|
||||
raise
|
||||
|
||||
if self.strip_after_prefix:
|
||||
view.skip_ws()
|
||||
|
||||
invoker = view.get_word()
|
||||
ctx.invoked_with = invoker
|
||||
ctx.prefix = invoked_prefix
|
||||
@ -949,7 +991,7 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.dispatch('command_completion', ctx)
|
||||
elif ctx.invoked_with:
|
||||
exc = errors.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with))
|
||||
exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found')
|
||||
self.dispatch('command_error', ctx, exc)
|
||||
|
||||
async def process_commands(self, message):
|
||||
@ -1054,6 +1096,12 @@ class Bot(BotBase, discord.Client):
|
||||
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
strip_after_prefix: :class:`bool`
|
||||
Whether to strip whitespace characters after encountering the command
|
||||
prefix. This allows for ``! hello`` and ``!hello`` to both work if
|
||||
the ``command_prefix`` is set to ``!``. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -101,7 +99,7 @@ class CogMeta(type):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, bases, attrs = args
|
||||
attrs['__cog_name__'] = kwargs.pop('name', name)
|
||||
attrs['__cog_settings__'] = command_attrs = kwargs.pop('command_attrs', {})
|
||||
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
|
||||
|
||||
aliases = kwargs.pop('aliases', [])
|
||||
if not isinstance(aliases, list):
|
||||
@ -130,13 +128,13 @@ class CogMeta(type):
|
||||
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))
|
||||
raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
commands[elem] = value
|
||||
elif inspect.iscoroutinefunction(value):
|
||||
try:
|
||||
is_listener = getattr(value, '__cog_listener__')
|
||||
getattr(value, '__cog_listener__')
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
@ -202,7 +200,7 @@ class Cog(metaclass=CogMeta):
|
||||
parent = lookup[parent.qualified_name]
|
||||
|
||||
# Update our parent's reference to our self
|
||||
removed = parent.remove_command(command.name)
|
||||
parent.remove_command(command.name)
|
||||
parent.add_command(command)
|
||||
|
||||
return self
|
||||
@ -285,7 +283,7 @@ class Cog(metaclass=CogMeta):
|
||||
"""
|
||||
|
||||
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))
|
||||
raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
|
||||
|
||||
def decorator(func):
|
||||
actual = func
|
||||
@ -306,6 +304,13 @@ class Cog(metaclass=CogMeta):
|
||||
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.
|
||||
@ -411,7 +416,8 @@ class Cog(metaclass=CogMeta):
|
||||
except Exception as e:
|
||||
# undo our additions
|
||||
for to_undo in self.__cog_commands__[:index]:
|
||||
bot.remove_command(to_undo.name)
|
||||
if to_undo.parent is None:
|
||||
bot.remove_command(to_undo.name)
|
||||
raise e
|
||||
|
||||
# check if we're overriding the default
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -28,6 +26,10 @@ import re
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
|
||||
__all__ = (
|
||||
'Context',
|
||||
)
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
r"""Represents the context in which a command is being invoked under.
|
||||
|
||||
@ -58,6 +60,14 @@ class Context(discord.abc.Messageable):
|
||||
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``.
|
||||
@ -80,6 +90,7 @@ class Context(discord.abc.Messageable):
|
||||
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)
|
||||
@ -94,7 +105,7 @@ class Context(discord.abc.Messageable):
|
||||
pattern = re.compile(r"<@!?%s>" % user.id)
|
||||
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
|
||||
|
||||
async def invoke(self, *args, **kwargs):
|
||||
async def invoke(self, command, /, *args, **kwargs):
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
@ -111,10 +122,6 @@ class Context(discord.abc.Messageable):
|
||||
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`
|
||||
@ -129,18 +136,12 @@ class Context(discord.abc.Messageable):
|
||||
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:])
|
||||
arguments.extend(args)
|
||||
|
||||
ret = await command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
@ -184,13 +185,15 @@ class Context(discord.abc.Messageable):
|
||||
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
|
||||
view.get_word() # advance to get the root command
|
||||
self.invoked_parents = []
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
@ -202,6 +205,7 @@ class Context(discord.abc.Messageable):
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.invoked_parents = invoked_parents
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
@ -214,7 +218,7 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
""":class:`.Cog`: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
"""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
|
||||
@ -227,8 +231,8 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
""":class:`.TextChannel`:
|
||||
Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`.
|
||||
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Message.channel`.
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@ -311,7 +315,7 @@ class Context(discord.abc.Messageable):
|
||||
entity = bot.get_cog(entity) or bot.get_command(entity)
|
||||
|
||||
try:
|
||||
qualified_name = entity.qualified_name
|
||||
entity.qualified_name
|
||||
except AttributeError:
|
||||
# if we're here then it's not a cog, group, or command.
|
||||
return None
|
||||
@ -333,7 +337,6 @@ class Context(discord.abc.Messageable):
|
||||
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)
|
||||
|
||||
reply.__doc__ = discord.Message.reply.__doc__
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,34 +22,44 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import inspect
|
||||
import typing
|
||||
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
|
||||
|
||||
import discord
|
||||
|
||||
from .errors import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
|
||||
__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:
|
||||
@ -60,9 +68,13 @@ def _get_from_guilds(bot, getter, argument):
|
||||
return result
|
||||
return result
|
||||
|
||||
_utils_get = discord.utils.get
|
||||
|
||||
class Converter:
|
||||
_utils_get = discord.utils.get
|
||||
T = TypeVar('T', covariant=True)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Converter(Protocol[T]):
|
||||
"""The base class of custom converters that require the :class:`.Context`
|
||||
to be passed to be useful.
|
||||
|
||||
@ -73,7 +85,7 @@ class Converter:
|
||||
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> T:
|
||||
"""|coro|
|
||||
|
||||
The method to override to do conversion logic.
|
||||
@ -98,15 +110,15 @@ class Converter:
|
||||
"""
|
||||
raise NotImplementedError('Derived classes need to implement this.')
|
||||
|
||||
class IDConverter(Converter):
|
||||
class IDConverter(Converter[T]):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r'([0-9]{15,21})$')
|
||||
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):
|
||||
class MemberConverter(IDConverter[discord.Member]):
|
||||
"""Converts to a :class:`~discord.Member`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -159,7 +171,7 @@ class MemberConverter(IDConverter):
|
||||
return None
|
||||
return members[0]
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Member:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
guild = ctx.guild
|
||||
@ -192,7 +204,7 @@ class MemberConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class UserConverter(IDConverter):
|
||||
class UserConverter(IDConverter[discord.User]):
|
||||
"""Converts to a :class:`~discord.User`.
|
||||
|
||||
All lookups are via the global user cache.
|
||||
@ -211,7 +223,7 @@ class UserConverter(IDConverter):
|
||||
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):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.User:
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
result = None
|
||||
state = ctx._state
|
||||
@ -251,7 +263,39 @@ class UserConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class MessageConverter(Converter):
|
||||
class PartialMessageConverter(Converter[discord.PartialMessage]):
|
||||
"""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
|
||||
"""
|
||||
@staticmethod
|
||||
def _get_id_matches(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: Context, argument: str) -> discord.PartialMessage:
|
||||
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(IDConverter[discord.Message]):
|
||||
"""Converts to a :class:`discord.Message`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
@ -265,22 +309,12 @@ class MessageConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,21}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
message_id = int(match.group("message_id"))
|
||||
channel_id = match.group("channel_id")
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Message:
|
||||
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
|
||||
message = ctx.bot._connection._get_message(message_id)
|
||||
if message:
|
||||
return message
|
||||
channel = ctx.bot.get_channel(int(channel_id)) if channel_id else ctx.channel
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
try:
|
||||
@ -290,7 +324,7 @@ class MessageConverter(Converter):
|
||||
except discord.Forbidden:
|
||||
raise ChannelNotReadable(channel)
|
||||
|
||||
class TextChannelConverter(IDConverter):
|
||||
class TextChannelConverter(IDConverter[discord.TextChannel]):
|
||||
"""Converts to a :class:`~discord.TextChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -305,7 +339,7 @@ class TextChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
@ -332,7 +366,7 @@ class TextChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class VoiceChannelConverter(IDConverter):
|
||||
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
|
||||
"""Converts to a :class:`~discord.VoiceChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -347,7 +381,7 @@ class VoiceChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
@ -373,7 +407,47 @@ class VoiceChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
class StageChannelConverter(IDConverter[discord.StageChannel]):
|
||||
"""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: Context, argument: str) -> discord.StageChannel:
|
||||
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[discord.CategoryChannel]):
|
||||
"""Converts to a :class:`~discord.CategoryChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -388,7 +462,7 @@ class CategoryChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
@ -415,7 +489,48 @@ class CategoryChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter):
|
||||
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
|
||||
"""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: Context, argument: str) -> discord.StoreChannel:
|
||||
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[discord.Colour]):
|
||||
"""Converts to a :class:`~discord.Colour`.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
@ -426,33 +541,80 @@ class ColourConverter(Converter):
|
||||
- ``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`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
arg = argument.replace('0x', '').lower()
|
||||
|
||||
if arg[0] == '#':
|
||||
arg = arg[1:]
|
||||
.. 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(arg)
|
||||
return discord.Colour(value=value)
|
||||
raise BadColourArgument(argument)
|
||||
except ValueError:
|
||||
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()
|
||||
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: Context, argument: str) -> discord.Colour:
|
||||
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):
|
||||
class RoleConverter(IDConverter[discord.Role]):
|
||||
"""Converts to a :class:`~discord.Role`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -467,7 +629,7 @@ class RoleConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Role:
|
||||
guild = ctx.guild
|
||||
if not guild:
|
||||
raise NoPrivateMessage()
|
||||
@ -482,12 +644,12 @@ class RoleConverter(IDConverter):
|
||||
raise RoleNotFound(argument)
|
||||
return result
|
||||
|
||||
class GameConverter(Converter):
|
||||
class GameConverter(Converter[discord.Game]):
|
||||
"""Converts to :class:`~discord.Game`."""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Game:
|
||||
return discord.Game(name=argument)
|
||||
|
||||
class InviteConverter(Converter):
|
||||
class InviteConverter(Converter[discord.Invite]):
|
||||
"""Converts to a :class:`~discord.Invite`.
|
||||
|
||||
This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
|
||||
@ -495,14 +657,40 @@ class InviteConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
|
||||
try:
|
||||
invite = await ctx.bot.fetch_invite(argument)
|
||||
return invite
|
||||
except Exception as exc:
|
||||
raise BadInviteArgument() from exc
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
class GuildConverter(IDConverter[discord.Guild]):
|
||||
"""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: Context, argument: str) -> discord.Guild:
|
||||
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[discord.Emoji]):
|
||||
"""Converts to a :class:`~discord.Emoji`.
|
||||
|
||||
All lookups are done for the local guild first, if available. If that lookup
|
||||
@ -517,7 +705,7 @@ class EmojiConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
|
||||
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
|
||||
result = None
|
||||
bot = ctx.bot
|
||||
@ -545,7 +733,7 @@ class EmojiConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class PartialEmojiConverter(Converter):
|
||||
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
|
||||
"""Converts to a :class:`~discord.PartialEmoji`.
|
||||
|
||||
This is done by extracting the animated flag, name and ID from the emoji.
|
||||
@ -553,7 +741,7 @@ class PartialEmojiConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
|
||||
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
|
||||
|
||||
if match:
|
||||
@ -566,7 +754,7 @@ class PartialEmojiConverter(Converter):
|
||||
|
||||
raise PartialEmojiConversionFailure(argument)
|
||||
|
||||
class clean_content(Converter):
|
||||
class clean_content(Converter[str]):
|
||||
"""Converts the argument to mention scrubbed version of
|
||||
said content.
|
||||
|
||||
@ -580,20 +768,25 @@ class clean_content(Converter):
|
||||
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):
|
||||
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):
|
||||
async def convert(self, ctx: Context, argument: str) -> str:
|
||||
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')
|
||||
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
|
||||
|
||||
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
||||
|
||||
@ -608,12 +801,12 @@ class clean_content(Converter):
|
||||
|
||||
|
||||
transformations.update(
|
||||
('<@%s>' % member_id, resolve_member(member_id))
|
||||
(f'<@{member_id}>', resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
transformations.update(
|
||||
('<@!%s>' % member_id, resolve_member(member_id))
|
||||
(f'<@!{member_id}>', resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
@ -623,7 +816,7 @@ class clean_content(Converter):
|
||||
return '@' + r.name if r else '@deleted-role'
|
||||
|
||||
transformations.update(
|
||||
('<@&%s>' % role_id, resolve_role(role_id))
|
||||
(f'<@&{role_id}>', resolve_role(role_id))
|
||||
for role_id in message.raw_role_mentions
|
||||
)
|
||||
|
||||
@ -635,6 +828,8 @@ class clean_content(Converter):
|
||||
|
||||
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)
|
||||
@ -656,10 +851,10 @@ class _Greedy:
|
||||
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__)
|
||||
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
|
||||
|
||||
if getattr(converter, '__origin__', None) is typing.Union and type(None) in converter.__args__:
|
||||
raise TypeError('Greedy[%r] is invalid.' % converter)
|
||||
if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
|
||||
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
||||
|
||||
return self.__class__(converter=converter)
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -66,6 +64,9 @@ class BucketType(Enum):
|
||||
# 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')
|
||||
@ -78,8 +79,8 @@ class Cooldown:
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
if not isinstance(self.type, BucketType):
|
||||
raise TypeError('Cooldown type must be a BucketType')
|
||||
if not callable(self.type):
|
||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
if not current:
|
||||
@ -151,7 +152,7 @@ class CooldownMapping:
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
return self._cooldown.type.get_key(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
|
||||
@ -252,7 +253,7 @@ class MaxConcurrency:
|
||||
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))
|
||||
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self.number, per=self.per, wait=self.wait)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,7 @@ import functools
|
||||
import inspect
|
||||
import typing
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
import discord
|
||||
|
||||
@ -158,7 +157,7 @@ class Command(_BaseCommand):
|
||||
isn't one.
|
||||
cog: Optional[:class:`Cog`]
|
||||
The cog that this command belongs to. ``None`` if there isn't one.
|
||||
checks: List[Callable[..., :class:`bool`]]
|
||||
checks: List[Callable[[:class:`.Context`], :class:`bool`]]
|
||||
A list of predicates that verifies if the command could be executed
|
||||
with the given :class:`.Context` as the sole parameter. If an exception
|
||||
is necessary to be thrown to signal failure, then one inherited from
|
||||
@ -301,17 +300,37 @@ class Command(_BaseCommand):
|
||||
signature = inspect.signature(function)
|
||||
self.params = signature.parameters.copy()
|
||||
|
||||
# PEP-563 allows postponing evaluation of annotations with a __future__
|
||||
# import. When postponed, Parameter.annotation will be a string and must
|
||||
# be replaced with the real value for the converters to work later on
|
||||
# see: https://bugs.python.org/issue41341
|
||||
resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
|
||||
|
||||
try:
|
||||
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
|
||||
except NameError as e:
|
||||
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
|
||||
|
||||
for key, value in self.params.items():
|
||||
if isinstance(value.annotation, str):
|
||||
self.params[key] = value = value.replace(annotation=eval(value.annotation, function.__globals__))
|
||||
# coalesce the forward references
|
||||
if key in type_hints:
|
||||
self.params[key] = value = value.replace(annotation=type_hints[key])
|
||||
|
||||
# fail early for when someone passes an unparameterized Greedy type
|
||||
if value.annotation is converters.Greedy:
|
||||
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
|
||||
|
||||
def _return_resolved(self, type, **kwargs):
|
||||
return type
|
||||
|
||||
def _recursive_resolve(self, type, *, globals=None):
|
||||
if not isinstance(type, typing.ForwardRef):
|
||||
return type
|
||||
|
||||
resolved = eval(type.__forward_arg__, globals)
|
||||
args = typing.get_args(resolved)
|
||||
for index, arg in enumerate(args):
|
||||
inner_resolve_result = self._recursive_resolve(arg, globals=globals)
|
||||
resolved[index] = inner_resolve_result
|
||||
return resolved
|
||||
|
||||
def add_check(self, func):
|
||||
"""Adds a check to the command.
|
||||
|
||||
@ -445,19 +464,13 @@ class Command(_BaseCommand):
|
||||
converter = getattr(converters, converter.__name__ + 'Converter', converter)
|
||||
|
||||
try:
|
||||
if inspect.isclass(converter):
|
||||
if issubclass(converter, converters.Converter):
|
||||
instance = converter()
|
||||
ret = await instance.convert(ctx, argument)
|
||||
return ret
|
||||
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
|
||||
if inspect.ismethod(converter.convert):
|
||||
return await converter.convert(ctx, argument)
|
||||
else:
|
||||
method = getattr(converter, 'convert', None)
|
||||
if method is not None and inspect.ismethod(method):
|
||||
ret = await method(ctx, argument)
|
||||
return ret
|
||||
return await converter().convert(ctx, argument)
|
||||
elif isinstance(converter, converters.Converter):
|
||||
ret = await converter.convert(ctx, argument)
|
||||
return ret
|
||||
return await converter.convert(ctx, argument)
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
@ -473,7 +486,7 @@ class Command(_BaseCommand):
|
||||
except AttributeError:
|
||||
name = converter.__class__.__name__
|
||||
|
||||
raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from exc
|
||||
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
||||
|
||||
async def do_conversion(self, ctx, converter, argument, param):
|
||||
try:
|
||||
@ -523,7 +536,7 @@ class Command(_BaseCommand):
|
||||
# The greedy converter is simple -- it keeps going until it fails in which case,
|
||||
# it undos the view ready for the next parameter to use instead
|
||||
if type(converter) is converters._Greedy:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
|
||||
@ -693,7 +706,7 @@ class Command(_BaseCommand):
|
||||
raise discord.ClientException(fmt.format(self))
|
||||
|
||||
for name, param in iterator:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
transformed = await self.transform(ctx, param)
|
||||
args.append(transformed)
|
||||
elif param.kind == param.KEYWORD_ONLY:
|
||||
@ -715,9 +728,8 @@ class Command(_BaseCommand):
|
||||
except RuntimeError:
|
||||
break
|
||||
|
||||
if not self.ignore_extra:
|
||||
if not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
if not self.ignore_extra and not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
|
||||
async def call_before_hooks(self, ctx):
|
||||
# now that we're done preparing we can call the pre-command hooks
|
||||
@ -776,7 +788,7 @@ class Command(_BaseCommand):
|
||||
ctx.command = self
|
||||
|
||||
if not await self.can_run(ctx):
|
||||
raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self))
|
||||
raise CheckFailure(f'The check functions for command {self.qualified_name} failed.')
|
||||
|
||||
if self._max_concurrency is not None:
|
||||
await self._max_concurrency.acquire(ctx)
|
||||
@ -904,6 +916,13 @@ class Command(_BaseCommand):
|
||||
self.on_error = coro
|
||||
return coro
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the command has an error handler registered.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return hasattr(self, 'on_error')
|
||||
|
||||
def before_invoke(self, coro):
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
|
||||
@ -1008,23 +1027,23 @@ class Command(_BaseCommand):
|
||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
||||
should_print = param.default if isinstance(param.default, str) else param.default is not None
|
||||
if should_print:
|
||||
result.append('[%s=%s]' % (name, param.default) if not greedy else
|
||||
'[%s=%s]...' % (name, param.default))
|
||||
result.append(f'[{name}={param.default}]' if not greedy else
|
||||
f'[{name}={param.default}]...')
|
||||
continue
|
||||
else:
|
||||
result.append('[%s]' % name)
|
||||
result.append(f'[{name}]')
|
||||
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
if self.require_var_positional:
|
||||
result.append('<%s...>' % name)
|
||||
result.append(f'<{name}...>')
|
||||
else:
|
||||
result.append('[%s...]' % name)
|
||||
result.append(f'[{name}...]')
|
||||
elif greedy:
|
||||
result.append('[%s]...' % name)
|
||||
result.append(f'[{name}]...')
|
||||
elif self._is_typing_optional(param.annotation):
|
||||
result.append('[%s]' % name)
|
||||
result.append(f'[{name}]')
|
||||
else:
|
||||
result.append('<%s>' % name)
|
||||
result.append(f'<{name}>')
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
@ -1056,14 +1075,14 @@ class Command(_BaseCommand):
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
raise DisabledCommand('{0.name} command is disabled'.format(self))
|
||||
raise DisabledCommand(f'{self.name} command is disabled')
|
||||
|
||||
original = ctx.command
|
||||
ctx.command = self
|
||||
|
||||
try:
|
||||
if not await ctx.bot.can_run(ctx):
|
||||
raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self))
|
||||
raise CheckFailure(f'The global check functions for command {self.qualified_name} failed.')
|
||||
|
||||
cog = self.cog
|
||||
if cog is not None:
|
||||
@ -1335,6 +1354,8 @@ class Group(GroupMixin, Command):
|
||||
injected = hooked_wrapped_callback(self, ctx, self.callback)
|
||||
await injected(*ctx.args, **ctx.kwargs)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.invoke(ctx)
|
||||
@ -1373,6 +1394,8 @@ class Group(GroupMixin, Command):
|
||||
if call_hooks:
|
||||
await self.call_after_hooks(ctx)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks)
|
||||
@ -1578,7 +1601,7 @@ def check_any(*checks):
|
||||
try:
|
||||
pred = wrapped.predicate
|
||||
except AttributeError:
|
||||
raise TypeError('%r must be wrapped by commands.check decorator' % wrapped) from None
|
||||
raise TypeError(f'{wrapped!r} must be wrapped by commands.check decorator') from None
|
||||
else:
|
||||
unwrapped.append(pred)
|
||||
|
||||
@ -1766,7 +1789,7 @@ def has_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
ch = ctx.channel
|
||||
@ -1791,7 +1814,7 @@ def bot_has_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
guild = ctx.guild
|
||||
@ -1819,7 +1842,7 @@ def has_guild_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
if not ctx.guild:
|
||||
@ -1844,7 +1867,7 @@ def bot_has_guild_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
if not ctx.guild:
|
||||
@ -1949,8 +1972,11 @@ def cooldown(rate, per, type=BucketType.default):
|
||||
The number of times a command can be used before triggering a cooldown.
|
||||
per: :class:`float`
|
||||
The amount of seconds to wait for a cooldown when it's been triggered.
|
||||
type: :class:`.BucketType`
|
||||
The type of cooldown to have.
|
||||
type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]]
|
||||
The type of cooldown to have. If callable, should return a key for the mapping.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Callables are now supported for custom bucket types.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -45,6 +43,7 @@ __all__ = (
|
||||
'NotOwner',
|
||||
'MessageNotFound',
|
||||
'MemberNotFound',
|
||||
'GuildNotFound',
|
||||
'UserNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelNotReadable',
|
||||
@ -142,7 +141,7 @@ class MissingRequiredArgument(UserInputError):
|
||||
"""
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
super().__init__('{0.name} is a required argument that is missing.'.format(param))
|
||||
super().__init__(f'{param.name} is a required argument that is missing.')
|
||||
|
||||
class TooManyArguments(UserInputError):
|
||||
"""Exception raised when the command was passed too many arguments and its
|
||||
@ -228,7 +227,23 @@ class MemberNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Member "{}" not found.'.format(argument))
|
||||
super().__init__(f'Member "{argument}" not found.')
|
||||
|
||||
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__(f'Guild "{argument}" not found.')
|
||||
|
||||
class UserNotFound(BadArgument):
|
||||
"""Exception raised when the user provided was not found in the bot's
|
||||
@ -245,7 +260,7 @@ class UserNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('User "{}" not found.'.format(argument))
|
||||
super().__init__(f'User "{argument}" not found.')
|
||||
|
||||
class MessageNotFound(BadArgument):
|
||||
"""Exception raised when the message provided was not found in the channel.
|
||||
@ -261,7 +276,7 @@ class MessageNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Message "{}" not found.'.format(argument))
|
||||
super().__init__(f'Message "{argument}" not found.')
|
||||
|
||||
class ChannelNotReadable(BadArgument):
|
||||
"""Exception raised when the bot does not have permission to read messages
|
||||
@ -278,7 +293,7 @@ class ChannelNotReadable(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__("Can't read messages in {}.".format(argument.mention))
|
||||
super().__init__(f"Can't read messages in {argument.mention}.")
|
||||
|
||||
class ChannelNotFound(BadArgument):
|
||||
"""Exception raised when the bot can not find the channel.
|
||||
@ -294,7 +309,7 @@ class ChannelNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Channel "{}" not found.'.format(argument))
|
||||
super().__init__(f'Channel "{argument}" not found.')
|
||||
|
||||
class BadColourArgument(BadArgument):
|
||||
"""Exception raised when the colour is not valid.
|
||||
@ -310,7 +325,7 @@ class BadColourArgument(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Colour "{}" is invalid.'.format(argument))
|
||||
super().__init__(f'Colour "{argument}" is invalid.')
|
||||
|
||||
BadColorArgument = BadColourArgument
|
||||
|
||||
@ -328,7 +343,7 @@ class RoleNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Role "{}" not found.'.format(argument))
|
||||
super().__init__(f'Role "{argument}" not found.')
|
||||
|
||||
class BadInviteArgument(BadArgument):
|
||||
"""Exception raised when the invite is invalid or expired.
|
||||
@ -354,7 +369,7 @@ class EmojiNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Emoji "{}" not found.'.format(argument))
|
||||
super().__init__(f'Emoji "{argument}" not found.')
|
||||
|
||||
class PartialEmojiConversionFailure(BadArgument):
|
||||
"""Exception raised when the emoji provided does not match the correct
|
||||
@ -371,7 +386,7 @@ class PartialEmojiConversionFailure(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
||||
super().__init__(f'Couldn\'t convert "{argument}" to PartialEmoji.')
|
||||
|
||||
class BadBoolArgument(BadArgument):
|
||||
"""Exception raised when a boolean argument was not convertable.
|
||||
@ -387,7 +402,7 @@ class BadBoolArgument(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('{} is not a recognised boolean option'.format(argument))
|
||||
super().__init__(f'{argument} is not a recognised boolean option')
|
||||
|
||||
class DisabledCommand(CommandError):
|
||||
"""Exception raised when the command being invoked is disabled.
|
||||
@ -427,7 +442,7 @@ class CommandOnCooldown(CommandError):
|
||||
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))
|
||||
super().__init__(f'You are on cooldown. Try again in {retry_after:.2f}s')
|
||||
|
||||
class MaxConcurrencyReached(CommandError):
|
||||
"""Exception raised when the command being invoked has reached its maximum concurrency.
|
||||
@ -449,7 +464,7 @@ class MaxConcurrencyReached(CommandError):
|
||||
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))
|
||||
super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
|
||||
|
||||
class MissingRole(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks a role to run a command.
|
||||
@ -466,7 +481,7 @@ class MissingRole(CheckFailure):
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Role {0!r} is required to run this command.'.format(missing_role)
|
||||
message = f'Role {missing_role!r} is required to run this command.'
|
||||
super().__init__(message)
|
||||
|
||||
class BotMissingRole(CheckFailure):
|
||||
@ -484,7 +499,7 @@ class BotMissingRole(CheckFailure):
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Bot requires the role {0!r} to run this command'.format(missing_role)
|
||||
message = f'Bot requires the role {missing_role!r} to run this command'
|
||||
super().__init__(message)
|
||||
|
||||
class MissingAnyRole(CheckFailure):
|
||||
@ -504,14 +519,14 @@ class MissingAnyRole(CheckFailure):
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
missing = [f"'{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)
|
||||
message = f"You are missing at least one of the required roles: {fmt}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@ -533,14 +548,14 @@ class BotMissingAnyRole(CheckFailure):
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
missing = [f"'{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)
|
||||
message = f"Bot is missing at least one of the required roles: {fmt}"
|
||||
super().__init__(message)
|
||||
|
||||
class NSFWChannelRequired(CheckFailure):
|
||||
@ -557,7 +572,7 @@ class NSFWChannelRequired(CheckFailure):
|
||||
"""
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
super().__init__("Channel '{}' needs to be NSFW for this command to work.".format(channel))
|
||||
super().__init__(f"Channel '{channel}' needs to be NSFW for this command to work.")
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks permissions to run a
|
||||
@ -579,7 +594,7 @@ class MissingPermissions(CheckFailure):
|
||||
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)
|
||||
message = f'You are missing {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
@ -602,7 +617,7 @@ class BotMissingPermissions(CheckFailure):
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'Bot requires {} permission(s) to run this command.'.format(fmt)
|
||||
message = f'Bot requires {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BadUnionArgument(UserInputError):
|
||||
@ -637,7 +652,7 @@ class BadUnionArgument(UserInputError):
|
||||
else:
|
||||
fmt = ' or '.join(to_string)
|
||||
|
||||
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
|
||||
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
|
||||
|
||||
class ArgumentParsingError(UserInputError):
|
||||
"""An exception raised when the parser fails to parse a user's input.
|
||||
@ -661,7 +676,7 @@ class UnexpectedQuoteError(ArgumentParsingError):
|
||||
"""
|
||||
def __init__(self, quote):
|
||||
self.quote = quote
|
||||
super().__init__('Unexpected quote mark, {0!r}, in non-quoted string'.format(quote))
|
||||
super().__init__(f'Unexpected quote mark, {quote!r}, in non-quoted string')
|
||||
|
||||
class InvalidEndOfQuotedStringError(ArgumentParsingError):
|
||||
"""An exception raised when a space is expected after the closing quote in a string
|
||||
@ -676,7 +691,7 @@ class InvalidEndOfQuotedStringError(ArgumentParsingError):
|
||||
"""
|
||||
def __init__(self, char):
|
||||
self.char = char
|
||||
super().__init__('Expected space after closing quotation but received {0!r}'.format(char))
|
||||
super().__init__(f'Expected space after closing quotation but received {char!r}')
|
||||
|
||||
class ExpectedClosingQuoteError(ArgumentParsingError):
|
||||
"""An exception raised when a quote character is expected but not found.
|
||||
@ -691,7 +706,7 @@ class ExpectedClosingQuoteError(ArgumentParsingError):
|
||||
|
||||
def __init__(self, close_quote):
|
||||
self.close_quote = close_quote
|
||||
super().__init__('Expected closing {}.'.format(close_quote))
|
||||
super().__init__(f'Expected closing {close_quote}.')
|
||||
|
||||
class ExtensionError(DiscordException):
|
||||
"""Base exception for extension related errors.
|
||||
@ -705,7 +720,7 @@ class ExtensionError(DiscordException):
|
||||
"""
|
||||
def __init__(self, message=None, *args, name):
|
||||
self.name = name
|
||||
message = message or 'Extension {!r} had an error.'.format(name)
|
||||
message = message or f'Extension {name!r} had an error.'
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
|
||||
super().__init__(m, *args)
|
||||
@ -716,7 +731,7 @@ class ExtensionAlreadyLoaded(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} is already loaded.'.format(name), name=name)
|
||||
super().__init__(f'Extension {name!r} is already loaded.', name=name)
|
||||
|
||||
class ExtensionNotLoaded(ExtensionError):
|
||||
"""An exception raised when an extension was not loaded.
|
||||
@ -724,7 +739,7 @@ class ExtensionNotLoaded(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} has not been loaded.'.format(name), name=name)
|
||||
super().__init__(f'Extension {name!r} has not been loaded.', name=name)
|
||||
|
||||
class NoEntryPointError(ExtensionError):
|
||||
"""An exception raised when an extension does not have a ``setup`` entry point function.
|
||||
@ -732,7 +747,7 @@ class NoEntryPointError(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name)
|
||||
super().__init__(f"Extension {name!r} has no 'setup' function.", name=name)
|
||||
|
||||
class ExtensionFailed(ExtensionError):
|
||||
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
|
||||
@ -791,4 +806,4 @@ class CommandRegistrationError(ClientException):
|
||||
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))
|
||||
super().__init__(f'The {type_} {name} is already an existing command or alias.')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -79,18 +77,22 @@ class Paginator:
|
||||
The suffix appended at the end of every page. e.g. three backticks.
|
||||
max_size: :class:`int`
|
||||
The maximum amount of codepoints allowed in a page.
|
||||
linesep: :class:`str`
|
||||
The character string inserted between lines. e.g. a newline character.
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000):
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size
|
||||
self.linesep = linesep
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
"""Clears the paginator to have no pages."""
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@ -104,6 +106,10 @@ class Paginator:
|
||||
def _suffix_len(self):
|
||||
return len(self.suffix) if self.suffix else 0
|
||||
|
||||
@property
|
||||
def _linesep_len(self):
|
||||
return len(self.linesep)
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
"""Adds a line to the current page.
|
||||
|
||||
@ -122,29 +128,29 @@ class Paginator:
|
||||
RuntimeError
|
||||
The line was too big for the current :attr:`max_size`.
|
||||
"""
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
|
||||
if len(line) > max_page_size:
|
||||
raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
|
||||
raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
|
||||
|
||||
if self._count + len(line) + 1 > self.max_size - self._suffix_len:
|
||||
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
|
||||
self.close_page()
|
||||
|
||||
self._count += len(line) + 1
|
||||
self._count += len(line) + self._linesep_len
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._count += 1
|
||||
self._count += self._linesep_len
|
||||
|
||||
def close_page(self):
|
||||
"""Prematurely terminate a page."""
|
||||
if self.suffix is not None:
|
||||
self._current_page.append(self.suffix)
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
self._pages.append(self.linesep.join(self._current_page))
|
||||
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@ -162,7 +168,7 @@ class Paginator:
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
|
||||
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
|
||||
return fmt.format(self)
|
||||
|
||||
def _not_overriden(f):
|
||||
@ -264,9 +270,13 @@ class HelpCommand:
|
||||
show_hidden: :class:`bool`
|
||||
Specifies if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
verify_checks: :class:`bool`
|
||||
verify_checks: Optional[:class:`bool`]
|
||||
Specifies if commands should have their :attr:`.Command.checks` called
|
||||
and verified. Defaults to ``True``.
|
||||
and verified. If ``True``, always calls :attr:`.Commands.checks`.
|
||||
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
|
||||
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
|
||||
|
||||
..versionchanged:: 1.7
|
||||
command_attrs: :class:`dict`
|
||||
A dictionary of options to pass in for the construction of the help command.
|
||||
This allows you to change the command behaviour without actually changing
|
||||
@ -309,7 +319,7 @@ class HelpCommand:
|
||||
attrs.setdefault('name', 'help')
|
||||
attrs.setdefault('help', 'Shows this message')
|
||||
self.context = None
|
||||
self._command_impl = None
|
||||
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
|
||||
|
||||
def copy(self):
|
||||
obj = self.__class__(*self.__original_args__, **self.__original_kwargs__)
|
||||
@ -324,7 +334,6 @@ class HelpCommand:
|
||||
def _remove_from_bot(self, bot):
|
||||
bot.remove_command(self._command_impl.name)
|
||||
self._command_impl._eject_cog()
|
||||
self._command_impl = None
|
||||
|
||||
def add_check(self, func):
|
||||
"""
|
||||
@ -338,13 +347,7 @@ class HelpCommand:
|
||||
The function that will be used as a check.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.add_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].append(func)
|
||||
except KeyError:
|
||||
self.command_attrs["checks"] = [func]
|
||||
self._command_impl.add_check(func)
|
||||
|
||||
def remove_check(self, func):
|
||||
"""
|
||||
@ -361,13 +364,7 @@ class HelpCommand:
|
||||
The function to remove from the checks.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.remove_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].remove(func)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
self._command_impl.remove_check(func)
|
||||
|
||||
def get_bot_mapping(self):
|
||||
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
||||
@ -376,7 +373,7 @@ class HelpCommand:
|
||||
cog: cog.get_commands()
|
||||
for cog in bot.cogs.values()
|
||||
}
|
||||
mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
|
||||
mapping[None] = [c for c in bot.commands if c.cog is None]
|
||||
return mapping
|
||||
|
||||
@property
|
||||
@ -387,8 +384,9 @@ class HelpCommand:
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
# for this common use case rather than waste performance for the
|
||||
# odd one.
|
||||
pattern = re.compile(r"<@!?%s>" % user.id)
|
||||
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.context.prefix)
|
||||
pattern = re.compile(fr"<@!?{user.id}>")
|
||||
display_name = user.display_name.replace('\\', r'\\')
|
||||
return pattern.sub('@' + display_name, self.context.prefix)
|
||||
|
||||
@property
|
||||
def invoked_with(self):
|
||||
@ -425,17 +423,26 @@ class HelpCommand:
|
||||
The signature for the command.
|
||||
"""
|
||||
|
||||
parent = command.full_parent_name
|
||||
parent = command.parent
|
||||
entries = []
|
||||
while parent is not None:
|
||||
if not parent.signature or parent.invoke_without_command:
|
||||
entries.append(parent.name)
|
||||
else:
|
||||
entries.append(parent.name + ' ' + parent.signature)
|
||||
parent = parent.parent
|
||||
parent_sig = ' '.join(reversed(entries))
|
||||
|
||||
if len(command.aliases) > 0:
|
||||
aliases = '|'.join(command.aliases)
|
||||
fmt = '[%s|%s]' % (command.name, aliases)
|
||||
if parent:
|
||||
fmt = parent + ' ' + fmt
|
||||
fmt = f'[{command.name}|{aliases}]'
|
||||
if parent_sig:
|
||||
fmt = parent_sig + ' ' + fmt
|
||||
alias = fmt
|
||||
else:
|
||||
alias = command.name if not parent else parent + ' ' + command.name
|
||||
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
||||
|
||||
return '%s%s %s' % (self.clean_prefix, alias, command.signature)
|
||||
return f'{self.clean_prefix}{alias} {command.signature}'
|
||||
|
||||
def remove_mentions(self, string):
|
||||
"""Removes mentions from the string to prevent abuse.
|
||||
@ -498,7 +505,7 @@ class HelpCommand:
|
||||
:class:`str`
|
||||
The string to use when a command has not been found.
|
||||
"""
|
||||
return 'No command called "{}" found.'.format(string)
|
||||
return f'No command called "{string}" found.'
|
||||
|
||||
def subcommand_not_found(self, command, string):
|
||||
"""|maybecoro|
|
||||
@ -527,8 +534,8 @@ class HelpCommand:
|
||||
The string to use when the command did not have the subcommand requested.
|
||||
"""
|
||||
if isinstance(command, Group) and len(command.all_commands) > 0:
|
||||
return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string)
|
||||
return 'Command "{0.qualified_name}" has no subcommands.'.format(command)
|
||||
return f'Command "{command.qualified_name}" has no subcommand named {string}'
|
||||
return f'Command "{command.qualified_name}" has no subcommands.'
|
||||
|
||||
async def filter_commands(self, commands, *, sort=False, key=None):
|
||||
"""|coro|
|
||||
@ -560,11 +567,15 @@ class HelpCommand:
|
||||
|
||||
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
|
||||
|
||||
if not self.verify_checks:
|
||||
if self.verify_checks is False:
|
||||
# if we do not need to verify the checks then we can just
|
||||
# run it straight through normally without using await.
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
if self.verify_checks is None and not self.context.guild:
|
||||
# if verify_checks is None and we're in a DM, don't verify
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
# if we're here then we need to check every command if it can run
|
||||
async def predicate(cmd):
|
||||
try:
|
||||
@ -929,8 +940,8 @@ class DefaultHelpCommand(HelpCommand):
|
||||
def get_ending_note(self):
|
||||
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||
command_name = self.invoked_with
|
||||
return "Type {0}{1} command for more info on a command.\n" \
|
||||
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
|
||||
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
|
||||
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
|
||||
|
||||
def add_indented_commands(self, commands, *, heading, max_size=None):
|
||||
"""Indents a list of commands after the specified heading.
|
||||
@ -965,7 +976,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
for command in commands:
|
||||
name = command.name
|
||||
width = max_size - (get_width(name) - len(name))
|
||||
entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width)
|
||||
entry = f'{self.indent * " "}{name:<{width}} {command.short_doc}'
|
||||
self.paginator.add_line(self.shorten_text(entry))
|
||||
|
||||
async def send_pages(self):
|
||||
@ -1018,7 +1029,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
# <description> portion
|
||||
self.paginator.add_line(bot.description, empty=True)
|
||||
|
||||
no_category = '\u200b{0.no_category}:'.format(self)
|
||||
no_category = f'\u200b{self.no_category}:'
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name + ':' if cog is not None else no_category
|
||||
@ -1142,7 +1153,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
|
||||
|
||||
def get_command_signature(self, command):
|
||||
return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
|
||||
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
|
||||
|
||||
def get_ending_note(self):
|
||||
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
||||
@ -1174,7 +1185,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if commands:
|
||||
# U+2002 Middle Dot
|
||||
joined = '\u2002'.join(c.name for c in commands)
|
||||
self.paginator.add_line('__**%s**__' % heading)
|
||||
self.paginator.add_line(f'__**{heading}**__')
|
||||
self.paginator.add_line(joined)
|
||||
|
||||
def add_subcommand_formatting(self, command):
|
||||
@ -1208,7 +1219,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
aliases: Sequence[:class:`str`]
|
||||
A list of aliases to format.
|
||||
"""
|
||||
self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True)
|
||||
self.paginator.add_line(f'**{self.aliases_heading}** {", ".join(aliases)}', empty=True)
|
||||
|
||||
def add_command_formatting(self, command):
|
||||
"""A utility function to format commands and groups.
|
||||
@ -1261,7 +1272,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
no_category = '\u200b{0.no_category}'.format(self)
|
||||
no_category = f'\u200b{self.no_category}'
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name if cog is not None else no_category
|
||||
@ -1294,7 +1305,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
|
||||
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
|
||||
if filtered:
|
||||
self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading))
|
||||
self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
@ -1314,7 +1325,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
self.paginator.add_line('**%s**' % self.commands_heading)
|
||||
self.paginator.add_line(f'**{self.commands_heading}**')
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -37,6 +35,10 @@ from discord.backoff import ExponentialBackoff
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = (
|
||||
'loop',
|
||||
)
|
||||
|
||||
class Loop:
|
||||
"""A background task helper that abstracts the loop and reconnection logic for you.
|
||||
|
||||
@ -103,7 +105,7 @@ class Loop:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if now > self._next_iteration:
|
||||
self._next_iteration = now
|
||||
except self._valid_exception as exc:
|
||||
except self._valid_exception:
|
||||
self._last_iteration_failed = True
|
||||
if not self.reconnect:
|
||||
raise
|
||||
@ -154,14 +156,14 @@ class Loop:
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._task is None and self._sleep:
|
||||
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):
|
||||
"""|coro|
|
||||
r"""|coro|
|
||||
|
||||
Calls the internal callback that the task holds.
|
||||
|
||||
@ -289,9 +291,9 @@ class Loop:
|
||||
|
||||
for exc in exceptions:
|
||||
if not inspect.isclass(exc):
|
||||
raise TypeError('{0!r} must be a class.'.format(exc))
|
||||
raise TypeError(f'{exc!r} must be a class.')
|
||||
if not issubclass(exc, BaseException):
|
||||
raise TypeError('{0!r} must inherit from BaseException.'.format(exc))
|
||||
raise TypeError(f'{exc!r} must inherit from BaseException.')
|
||||
|
||||
self._valid_exception = (*self._valid_exception, *exceptions)
|
||||
|
||||
@ -345,7 +347,7 @@ class Loop:
|
||||
|
||||
async def _error(self, *args):
|
||||
exception = args[-1]
|
||||
print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr)
|
||||
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
def before_loop(self, coro):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
import os.path
|
||||
import io
|
||||
|
||||
__all__ = (
|
||||
'File',
|
||||
)
|
||||
|
||||
class File:
|
||||
r"""A parameter object used for :meth:`abc.Messageable.send`
|
||||
for sending file objects.
|
||||
@ -58,14 +60,14 @@ class File:
|
||||
Whether the attachment is a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = ('fp', 'filename', '_original_pos', '_owner', '_closer')
|
||||
__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))
|
||||
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
|
||||
self.fp = fp
|
||||
self._original_pos = fp.tell()
|
||||
self._owner = False
|
||||
@ -92,6 +94,8 @@ class File:
|
||||
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
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, ClassVar, Dict, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, overload
|
||||
|
||||
from .enums import UserFlags
|
||||
|
||||
__all__ = (
|
||||
@ -34,27 +36,38 @@ __all__ = (
|
||||
'MemberCacheFlags',
|
||||
)
|
||||
|
||||
class flag_value:
|
||||
def __init__(self, func):
|
||||
FV = TypeVar('FV', bound='flag_value')
|
||||
BF = TypeVar('BF', bound='BaseFlags')
|
||||
|
||||
class flag_value(Generic[BF]):
|
||||
def __init__(self, func: Callable[[Any], int]):
|
||||
self.flag = func(None)
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
@overload
|
||||
def __get__(self: FV, instance: None, owner: Type[BF]) -> FV:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: BF, owner: Type[BF]) -> bool:
|
||||
...
|
||||
|
||||
def __get__(self, instance: Optional[BF], owner: Type[BF]) -> Any:
|
||||
if instance is None:
|
||||
return self
|
||||
return instance._has_flag(self.flag)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
def __set__(self, instance: BF, value: bool) -> None:
|
||||
instance._set_flag(self.flag, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<flag_value flag={.flag!r}>'.format(self)
|
||||
return f'<flag_value flag={self.flag!r}>'
|
||||
|
||||
class alias_flag_value(flag_value):
|
||||
pass
|
||||
|
||||
def fill_with_flags(*, inverted=False):
|
||||
def decorator(cls):
|
||||
def fill_with_flags(*, inverted: bool = False):
|
||||
def decorator(cls: Type[BF]):
|
||||
cls.VALID_FLAGS = {
|
||||
name: value.flag
|
||||
for name, value in cls.__dict__.items()
|
||||
@ -72,13 +85,18 @@ def fill_with_flags(*, inverted=False):
|
||||
|
||||
# n.b. flags must inherit from this and use the decorator above
|
||||
class BaseFlags:
|
||||
VALID_FLAGS: ClassVar[Dict[str, int]]
|
||||
DEFAULT_VALUE: ClassVar[int]
|
||||
|
||||
value: int
|
||||
|
||||
__slots__ = ('value',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
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)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@ -87,19 +105,19 @@ class BaseFlags:
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, self.__class__) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s value=%s>' % (self.__class__.__name__, self.value)
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} value={self.value}>'
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tuple[str, bool]]:
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, alias_flag_value):
|
||||
continue
|
||||
@ -107,16 +125,16 @@ class BaseFlags:
|
||||
if isinstance(value, flag_value):
|
||||
yield (name, self._has_flag(value.flag))
|
||||
|
||||
def _has_flag(self, o):
|
||||
def _has_flag(self, o: int) -> bool:
|
||||
return (self.value & o) == o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
def _set_flag(self, o: int, toggle: bool) -> None:
|
||||
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__)
|
||||
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
|
||||
|
||||
@fill_with_flags(inverted=True)
|
||||
class SystemChannelFlags(BaseFlags):
|
||||
@ -152,6 +170,7 @@ class SystemChannelFlags(BaseFlags):
|
||||
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"
|
||||
@ -159,10 +178,10 @@ class SystemChannelFlags(BaseFlags):
|
||||
# Since this is counter-intuitive from an API perspective and annoying
|
||||
# these will be inverted automatically
|
||||
|
||||
def _has_flag(self, o):
|
||||
def _has_flag(self, o: int) -> bool:
|
||||
return (self.value & o) != o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
def _set_flag(self, o: int, toggle: bool) -> None:
|
||||
if toggle is True:
|
||||
self.value &= ~o
|
||||
elif toggle is False:
|
||||
@ -212,6 +231,7 @@ class MessageFlags(BaseFlags):
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@flag_value
|
||||
@ -348,7 +368,7 @@ class PublicUserFlags(BaseFlags):
|
||||
"""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
def all(self):
|
||||
def all(self) -> List[UserFlags]:
|
||||
"""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)]
|
||||
|
||||
@ -395,11 +415,11 @@ class Intents(BaseFlags):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
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)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@ -419,7 +439,7 @@ class Intents(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
@ -428,14 +448,14 @@ class Intents(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[Intents]) -> Intents:
|
||||
"""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):
|
||||
def default(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||
except :attr:`presences` and :attr:`members`.
|
||||
"""
|
||||
@ -492,7 +512,7 @@ class Intents(BaseFlags):
|
||||
- :attr:`Member.nick`
|
||||
- :attr:`Member.premium_since`
|
||||
- :attr:`User.name`
|
||||
- :attr:`User.avatar` (:meth:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :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>`.
|
||||
@ -844,16 +864,16 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
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)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
@ -862,7 +882,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything disabled."""
|
||||
self = cls.__new__(cls)
|
||||
self.value = self.DEFAULT_VALUE
|
||||
@ -905,7 +925,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def from_intents(cls, intents):
|
||||
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` based on
|
||||
the currently selected :class:`Intents`.
|
||||
|
||||
@ -933,7 +953,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
return self
|
||||
|
||||
def _verify_intents(self, intents):
|
||||
def _verify_intents(self, intents: Intents):
|
||||
if self.online and not intents.presences:
|
||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
||||
|
||||
@ -954,4 +974,4 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
@property
|
||||
def _online_only(self):
|
||||
return self.value == 1
|
||||
return self.value == 1
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -162,8 +160,8 @@ class KeepAliveHandler(threading.Thread):
|
||||
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))
|
||||
stack = ''.join(traceback.format_stack(frame))
|
||||
msg = f'{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}'
|
||||
log.warning(msg, self.shard_id, total)
|
||||
|
||||
except Exception:
|
||||
@ -380,9 +378,6 @@ class DiscordWebSocket:
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
@ -422,16 +417,11 @@ class DiscordWebSocket:
|
||||
if type(msg) is bytes:
|
||||
self._buffer.extend(msg)
|
||||
|
||||
if len(msg) >= 4:
|
||||
if msg[-4:] == b'\x00\x00\xff\xff':
|
||||
msg = self._zlib.decompress(self._buffer)
|
||||
msg = msg.decode('utf-8')
|
||||
self._buffer = bytearray()
|
||||
else:
|
||||
return
|
||||
else:
|
||||
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)
|
||||
@ -629,13 +619,6 @@ class DiscordWebSocket:
|
||||
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,
|
||||
@ -876,7 +859,7 @@ class DiscordVoiceWebSocket:
|
||||
def average_latency(self):
|
||||
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
||||
heartbeat = self._keep_alive
|
||||
if heartbeat is None:
|
||||
if heartbeat is None or not heartbeat.recent_ack_latencies:
|
||||
return float('inf')
|
||||
|
||||
return sum(heartbeat.recent_ack_latencies) / len(heartbeat.recent_ack_latencies)
|
||||
|
207
discord/guild.py
207
discord/guild.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -46,6 +44,9 @@ from .asset import Asset
|
||||
from .flags import SystemChannelFlags
|
||||
from .integrations import Integration
|
||||
|
||||
__all__ = (
|
||||
'Guild',
|
||||
)
|
||||
|
||||
BanEntry = namedtuple('BanEntry', 'reason user')
|
||||
_GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize')
|
||||
@ -205,18 +206,21 @@ class Guild(Hashable):
|
||||
self._members.pop(member.id, None)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name or ''
|
||||
|
||||
def __int__(self):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
'id', 'name', 'shard_id', 'chunked'
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('shard_id', self.shard_id),
|
||||
('chunked', self.chunked),
|
||||
('member_count', getattr(self, '_member_count', None)),
|
||||
)
|
||||
resolved = ['%s=%r' % (attr, getattr(self, attr)) for attr in attrs]
|
||||
resolved.append('member_count=%r' % getattr(self, '_member_count', None))
|
||||
return '<Guild %s>' % ' '.join(resolved)
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<Guild {inner}>'
|
||||
|
||||
def _update_voice_state(self, data, channel_id):
|
||||
user_id = int(data['user_id'])
|
||||
@ -374,6 +378,18 @@ class Guild(Hashable):
|
||||
r.sort(key=lambda c: (c.position, c.id))
|
||||
return r
|
||||
|
||||
@property
|
||||
def stage_channels(self):
|
||||
"""List[:class:`StageChannel`]: A list of voice channels that belongs to this guild.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
This is sorted by the position and are in UI order from top to bottom.
|
||||
"""
|
||||
r = [ch for ch in self._channels.values() if isinstance(ch, StageChannel)]
|
||||
r.sort(key=lambda c: (c.position, c.id))
|
||||
return r
|
||||
|
||||
@property
|
||||
def me(self):
|
||||
""":class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`.
|
||||
@ -861,6 +877,13 @@ class Guild(Hashable):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
rtc_region = options.pop('rtc_region')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
||||
|
||||
parent_id = category.id if category else None
|
||||
return self._state.http.create_channel(self.id, channel_type.value, name=name, parent_id=parent_id,
|
||||
permission_overwrites=perms, **options)
|
||||
@ -962,6 +985,11 @@ class Guild(Hashable):
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a voice channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -984,6 +1012,38 @@ class Guild(Hashable):
|
||||
self._channels[channel.id] = channel
|
||||
return channel
|
||||
|
||||
async def create_stage_channel(self, name, *, topic=None, category=None, overwrites=None, reason=None, position=None):
|
||||
"""|coro|
|
||||
|
||||
This is similar to :meth:`create_text_channel` except makes a :class:`StageChannel` instead.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``slowmode_delay`` and ``nsfw`` parameters are not supported in this function.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have the proper permissions to create this channel.
|
||||
HTTPException
|
||||
Creating the channel failed.
|
||||
InvalidArgument
|
||||
The permission overwrite information is not in proper form.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`StageChannel`
|
||||
The channel that was just created.
|
||||
"""
|
||||
data = await self._create_channel(name, overwrites, ChannelType.stage_voice, category, reason=reason, position=position, topic=topic)
|
||||
channel = StageChannel(state=self._state, guild=self, data=data)
|
||||
|
||||
# temporarily add to the cache
|
||||
self._channels[channel.id] = channel
|
||||
return channel
|
||||
|
||||
async def create_category(self, name, *, overwrites=None, reason=None, position=None):
|
||||
"""|coro|
|
||||
|
||||
@ -1278,9 +1338,7 @@ class Guild(Hashable):
|
||||
return [convert(d) for d in data]
|
||||
|
||||
def fetch_members(self, *, limit=1000, after=None):
|
||||
"""|coro|
|
||||
|
||||
Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this,
|
||||
"""Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this,
|
||||
:meth:`Intents.members` must be enabled.
|
||||
|
||||
.. note::
|
||||
@ -1298,7 +1356,8 @@ class Guild(Hashable):
|
||||
Pass ``None`` to fetch all members. Note that this is potentially slow.
|
||||
after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve members after this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -1370,7 +1429,7 @@ class Guild(Hashable):
|
||||
async def fetch_member(self, member_id):
|
||||
"""|coro|
|
||||
|
||||
Retreives a :class:`Member` from a guild ID, and a member ID.
|
||||
Retrieves a :class:`Member` from a guild ID, and a member ID.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -1399,9 +1458,7 @@ class Guild(Hashable):
|
||||
async def fetch_ban(self, user):
|
||||
"""|coro|
|
||||
|
||||
Retrieves the :class:`BanEntry` for a user, which is a namedtuple
|
||||
with a ``user`` and ``reason`` field. See :meth:`bans` for more
|
||||
information.
|
||||
Retrieves the :class:`BanEntry` for a user.
|
||||
|
||||
You must have the :attr:`~Permissions.ban_members` permission
|
||||
to get this information.
|
||||
@ -1422,8 +1479,8 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
-------
|
||||
BanEntry
|
||||
The BanEntry object for the specified user.
|
||||
:class:`BanEntry`
|
||||
The :class:`BanEntry` object for the specified user.
|
||||
"""
|
||||
data = await self._state.http.get_ban(user.id, self.id)
|
||||
return BanEntry(
|
||||
@ -1434,12 +1491,7 @@ class Guild(Hashable):
|
||||
async def bans(self):
|
||||
"""|coro|
|
||||
|
||||
Retrieves all the users that are banned from the guild.
|
||||
|
||||
This coroutine returns a :class:`list` of BanEntry objects, which is a
|
||||
namedtuple with a ``user`` field to denote the :class:`User`
|
||||
that got banned along with a ``reason`` field specifying
|
||||
why the user was banned that could be set to ``None``.
|
||||
Retrieves all the users that are banned from the guild as a :class:`list` of :class:`BanEntry`.
|
||||
|
||||
You must have the :attr:`~Permissions.ban_members` permission
|
||||
to get this information.
|
||||
@ -1453,8 +1505,8 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[BanEntry]
|
||||
A list of BanEntry objects.
|
||||
List[:class:`BanEntry`]
|
||||
A list of :class:`BanEntry` objects.
|
||||
"""
|
||||
|
||||
data = await self._state.http.get_bans(self.id)
|
||||
@ -1513,7 +1565,7 @@ class Guild(Hashable):
|
||||
"""
|
||||
|
||||
if not isinstance(days, int):
|
||||
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
|
||||
raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
|
||||
|
||||
if roles:
|
||||
roles = [str(role.id) for role in roles]
|
||||
@ -1521,6 +1573,29 @@ class Guild(Hashable):
|
||||
data = await self._state.http.prune_members(self.id, days, compute_prune_count=compute_prune_count, roles=roles, reason=reason)
|
||||
return data['pruned']
|
||||
|
||||
async def templates(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the list of templates from this guild.
|
||||
|
||||
Requires :attr:`~.Permissions.manage_guild` permissions.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You don't have permissions to get the templates.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`Template`]
|
||||
The templates for this guild.
|
||||
"""
|
||||
from .template import Template
|
||||
data = await self._state.http.guild_templates(self.id)
|
||||
return [Template(data=d, state=self._state) for d in data]
|
||||
|
||||
async def webhooks(self):
|
||||
"""|coro|
|
||||
|
||||
@ -1543,7 +1618,7 @@ class Guild(Hashable):
|
||||
data = await self._state.http.guild_webhooks(self.id)
|
||||
return [Webhook.from_state(d, state=self._state) for d in data]
|
||||
|
||||
async def estimate_pruned_members(self, *, days):
|
||||
async def estimate_pruned_members(self, *, days, roles=None):
|
||||
"""|coro|
|
||||
|
||||
Similar to :meth:`prune_members` except instead of actually
|
||||
@ -1554,6 +1629,11 @@ class Guild(Hashable):
|
||||
-----------
|
||||
days: :class:`int`
|
||||
The number of days before counting as inactive.
|
||||
roles: Optional[List[:class:`abc.Snowflake`]]
|
||||
A list of :class:`abc.Snowflake` that represent roles to include in the estimate. If a member
|
||||
has a role that is not specified, they'll be excluded.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -1571,9 +1651,12 @@ class Guild(Hashable):
|
||||
"""
|
||||
|
||||
if not isinstance(days, int):
|
||||
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
|
||||
raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
|
||||
|
||||
data = await self._state.http.estimate_pruned_members(self.id, days)
|
||||
if roles:
|
||||
roles = [str(role.id) for role in roles]
|
||||
|
||||
data = await self._state.http.estimate_pruned_members(self.id, days, roles)
|
||||
return data['pruned']
|
||||
|
||||
async def invites(self):
|
||||
@ -1607,6 +1690,36 @@ class Guild(Hashable):
|
||||
|
||||
return result
|
||||
|
||||
async def create_template(self, *, name, description=None):
|
||||
"""|coro|
|
||||
|
||||
Creates a template for the guild.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the template.
|
||||
description: Optional[:class:`str`]
|
||||
The description of the template.
|
||||
"""
|
||||
from .template import Template
|
||||
|
||||
payload = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
if description:
|
||||
payload['description'] = description
|
||||
|
||||
data = await self._state.http.create_template(self.id, payload)
|
||||
|
||||
return Template(state=self._state, data=data)
|
||||
|
||||
async def create_integration(self, *, type, id):
|
||||
"""|coro|
|
||||
|
||||
@ -1786,6 +1899,9 @@ class Guild(Hashable):
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
do this.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
Can now pass ``int`` to ``colour`` keyword-only parameter.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -1838,7 +1954,7 @@ class Guild(Hashable):
|
||||
valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable')
|
||||
for key in fields:
|
||||
if key not in valid_keys:
|
||||
raise InvalidArgument('%r is not a valid field.' % key)
|
||||
raise InvalidArgument(f'{key!r} is not a valid field.')
|
||||
|
||||
data = await self._state.http.create_role(self.id, reason=reason, **fields)
|
||||
role = Role(guild=self, data=data, state=self._state)
|
||||
@ -2031,26 +2147,6 @@ class Guild(Hashable):
|
||||
payload['max_age'] = 0
|
||||
return Invite(state=self._state, data=payload)
|
||||
|
||||
def ack(self):
|
||||
"""|coro|
|
||||
|
||||
Marks every message in this guild as read.
|
||||
|
||||
The user must not be a bot user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Acking failed.
|
||||
ClientException
|
||||
You must not be a bot user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
if state.is_bot:
|
||||
raise ClientException('Must not be a bot account to ack messages.')
|
||||
return state.http.ack_guild(self.id)
|
||||
|
||||
def audit_logs(self, *, limit=100, before=None, after=None, oldest_first=None, user=None, action=None):
|
||||
"""Returns an :class:`AsyncIterator` that enables receiving the guild's audit logs.
|
||||
|
||||
@ -2080,10 +2176,12 @@ class Guild(Hashable):
|
||||
The number of entries to retrieve. If ``None`` retrieve all entries.
|
||||
before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve entries before this date or entry.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve entries after this date or entry.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
oldest_first: :class:`bool`
|
||||
If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if
|
||||
``after`` is specified, otherwise ``False``.
|
||||
@ -2224,6 +2322,9 @@ class Guild(Hashable):
|
||||
if user_ids is not None and query is not None:
|
||||
raise ValueError('Cannot pass both query and user_ids')
|
||||
|
||||
if user_ids is not None and not user_ids:
|
||||
raise ValueError('user_ids must contain at least 1 value')
|
||||
|
||||
limit = min(100, limit or 5)
|
||||
return await self._state.query_members(self, query=query, limit=limit, user_ids=user_ids, presences=presences, cache=cache)
|
||||
|
||||
|
705
discord/http.py
705
discord/http.py
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -30,6 +28,11 @@ from .user import User
|
||||
from .errors import InvalidArgument
|
||||
from .enums import try_enum, ExpireBehaviour
|
||||
|
||||
__all__ = (
|
||||
'IntegrationAccount',
|
||||
'Integration',
|
||||
)
|
||||
|
||||
class IntegrationAccount:
|
||||
"""Represents an integration account.
|
||||
|
||||
@ -84,7 +87,7 @@ class Integration:
|
||||
account: :class:`IntegrationAccount`
|
||||
The integration account information.
|
||||
synced_at: :class:`datetime.datetime`
|
||||
When the integration was last synced.
|
||||
An aware UTC datetime representing when the integration was last synced.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
|
||||
@ -186,7 +189,7 @@ class Integration:
|
||||
Syncing the integration failed.
|
||||
"""
|
||||
await self._state.http.sync_integration(self.guild.id, self.id)
|
||||
self.synced_at = datetime.datetime.utcnow()
|
||||
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
104
discord/interactions.py
Normal file
104
discord/interactions.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- 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 __future__ import annotations
|
||||
|
||||
from . import utils
|
||||
from .enums import try_enum, InteractionType
|
||||
|
||||
__all__ = (
|
||||
'Interaction',
|
||||
)
|
||||
|
||||
class Interaction:
|
||||
"""Represents a Discord interaction.
|
||||
|
||||
An interaction happens when a user does an action that needs to
|
||||
be notified. Current examples are slash commands but future examples
|
||||
include forms and buttons.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The interaction's ID.
|
||||
type: :class:`InteractionType`
|
||||
The interaction type.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID the interaction was sent from.
|
||||
channel_id: Optional[:class:`int`]
|
||||
The channel ID the interaction was sent from.
|
||||
application_id: :class:`int`
|
||||
The application ID that the interaction was for.
|
||||
user: Optional[Union[:class:`User`, :class:`Member`]]
|
||||
The user or member that sent the interaction.
|
||||
token: :class:`str`
|
||||
The token to continue the interaction. These are valid
|
||||
for 15 minutes.
|
||||
"""
|
||||
__slots__ = (
|
||||
'id',
|
||||
'type',
|
||||
'guild_id',
|
||||
'channel_id',
|
||||
'data',
|
||||
'application_id',
|
||||
'user',
|
||||
'token',
|
||||
'version',
|
||||
'_state',
|
||||
)
|
||||
|
||||
def __init__(self, *, data, state=None):
|
||||
self._state = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.id = int(data['id'])
|
||||
self.type = try_enum(InteractionType, data['type'])
|
||||
self.data = data.get('data')
|
||||
self.token = data['token']
|
||||
self.version = data['version']
|
||||
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
|
||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
self.application_id = utils._get_as_snowflake(data, 'application_id')
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
|
||||
return self._state and self._state.get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
|
||||
|
||||
Note that due to a Discord limitation, DM channels are not resolved since there is
|
||||
no data to complete them.
|
||||
"""
|
||||
guild = self.guild
|
||||
return guild and guild.get_channel(self.channel_id)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -30,6 +28,12 @@ from .object import Object
|
||||
from .mixins import Hashable
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
|
||||
__all__ = (
|
||||
'PartialInviteChannel',
|
||||
'PartialInviteGuild',
|
||||
'Invite',
|
||||
)
|
||||
|
||||
class PartialInviteChannel:
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
@ -80,7 +84,7 @@ class PartialInviteChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -258,7 +262,8 @@ class Invite(Hashable):
|
||||
Attributes
|
||||
-----------
|
||||
max_age: :class:`int`
|
||||
How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire.
|
||||
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`]]
|
||||
@ -266,7 +271,7 @@ class Invite(Hashable):
|
||||
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.
|
||||
An aware UTC 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.
|
||||
@ -274,6 +279,7 @@ class Invite(Hashable):
|
||||
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`]
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,20 +22,43 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
|
||||
|
||||
from .errors import NoMoreItems
|
||||
from .utils import time_snowflake, maybe_coroutine
|
||||
from .object import Object
|
||||
from .audit_logs import AuditLogEntry
|
||||
|
||||
__all__ = (
|
||||
'ReactionIterator',
|
||||
'HistoryIterator',
|
||||
'AuditLogIterator',
|
||||
'GuildIterator',
|
||||
'MemberIterator',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .member import Member
|
||||
from .user import User
|
||||
from .message import Message
|
||||
from .audit_logs import AuditLogEntry
|
||||
from .guild import Guild
|
||||
|
||||
T = TypeVar('T')
|
||||
OT = TypeVar('OT')
|
||||
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
|
||||
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
|
||||
|
||||
OLDEST_OBJECT = Object(id=0)
|
||||
|
||||
class _AsyncIterator:
|
||||
class _AsyncIterator(AsyncIterator[T]):
|
||||
__slots__ = ()
|
||||
|
||||
def get(self, **attrs):
|
||||
def get(self, **attrs: Any) -> Optional[T]:
|
||||
def predicate(elem):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split('__')
|
||||
@ -51,7 +72,7 @@ class _AsyncIterator:
|
||||
|
||||
return self.find(predicate)
|
||||
|
||||
async def find(self, predicate):
|
||||
async def find(self, predicate: _Predicate[T]) -> Optional[T]:
|
||||
while True:
|
||||
try:
|
||||
elem = await self.next()
|
||||
@ -62,47 +83,35 @@ class _AsyncIterator:
|
||||
if ret:
|
||||
return elem
|
||||
|
||||
def chunk(self, max_size):
|
||||
def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
|
||||
if max_size <= 0:
|
||||
raise ValueError('async iterator chunk sizes must be greater than 0.')
|
||||
return _ChunkedAsyncIterator(self, max_size)
|
||||
|
||||
def map(self, func):
|
||||
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
|
||||
return _MappedAsyncIterator(self, func)
|
||||
|
||||
def filter(self, predicate):
|
||||
def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
|
||||
return _FilteredAsyncIterator(self, predicate)
|
||||
|
||||
async def flatten(self):
|
||||
ret = []
|
||||
while True:
|
||||
try:
|
||||
item = await self.next()
|
||||
except NoMoreItems:
|
||||
return ret
|
||||
else:
|
||||
ret.append(item)
|
||||
async def flatten(self) -> List[T]:
|
||||
return [element async for element in self]
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
async def __anext__(self) -> T:
|
||||
try:
|
||||
msg = await self.next()
|
||||
return await self.next()
|
||||
except NoMoreItems:
|
||||
raise StopAsyncIteration()
|
||||
else:
|
||||
return msg
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
class _ChunkedAsyncIterator(_AsyncIterator):
|
||||
class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, max_size):
|
||||
self.iterator = iterator
|
||||
self.max_size = max_size
|
||||
|
||||
async def next(self):
|
||||
|
||||
async def next(self) -> T:
|
||||
ret = []
|
||||
n = 0
|
||||
while n < self.max_size:
|
||||
@ -117,17 +126,17 @@ class _ChunkedAsyncIterator(_AsyncIterator):
|
||||
n += 1
|
||||
return ret
|
||||
|
||||
class _MappedAsyncIterator(_AsyncIterator):
|
||||
class _MappedAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, func):
|
||||
self.iterator = iterator
|
||||
self.func = func
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
# this raises NoMoreItems and will propagate appropriately
|
||||
item = await self.iterator.next()
|
||||
return await maybe_coroutine(self.func, item)
|
||||
|
||||
class _FilteredAsyncIterator(_AsyncIterator):
|
||||
class _FilteredAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, predicate):
|
||||
self.iterator = iterator
|
||||
|
||||
@ -136,7 +145,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
|
||||
|
||||
self.predicate = predicate
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
getter = self.iterator.next
|
||||
pred = self.predicate
|
||||
while True:
|
||||
@ -146,7 +155,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
|
||||
if ret:
|
||||
return item
|
||||
|
||||
class ReactionIterator(_AsyncIterator):
|
||||
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||
def __init__(self, message, emoji, limit=100, after=None):
|
||||
self.message = message
|
||||
self.limit = limit
|
||||
@ -159,7 +168,7 @@ class ReactionIterator(_AsyncIterator):
|
||||
self.channel_id = message.channel.id
|
||||
self.users = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.users.empty():
|
||||
await self.fill_users()
|
||||
|
||||
@ -194,7 +203,7 @@ class ReactionIterator(_AsyncIterator):
|
||||
else:
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
|
||||
class HistoryIterator(_AsyncIterator):
|
||||
class HistoryIterator(_AsyncIterator['Message']):
|
||||
"""Iterator for receiving a channel's message history.
|
||||
|
||||
The messages endpoint has two behaviours we care about here:
|
||||
@ -280,7 +289,7 @@ class HistoryIterator(_AsyncIterator):
|
||||
if (self.after and self.after != OLDEST_OBJECT):
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.messages.empty():
|
||||
await self.fill_messages()
|
||||
|
||||
@ -291,36 +300,13 @@ class HistoryIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
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
|
||||
@ -374,7 +360,7 @@ class HistoryIterator(_AsyncIterator):
|
||||
return data
|
||||
return []
|
||||
|
||||
class AuditLogIterator(_AsyncIterator):
|
||||
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
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))
|
||||
@ -436,7 +422,7 @@ class AuditLogIterator(_AsyncIterator):
|
||||
self.after = Object(id=int(entries[0]['id']))
|
||||
return data.get('users', []), entries
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.entries.empty():
|
||||
await self._fill()
|
||||
|
||||
@ -447,13 +433,10 @@ class AuditLogIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
@ -482,7 +465,7 @@ class AuditLogIterator(_AsyncIterator):
|
||||
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
|
||||
|
||||
|
||||
class GuildIterator(_AsyncIterator):
|
||||
class GuildIterator(_AsyncIterator['Guild']):
|
||||
"""Iterator for receiving the client's guilds.
|
||||
|
||||
The guilds endpoint has the same two behaviours as described
|
||||
@ -536,7 +519,7 @@ class GuildIterator(_AsyncIterator):
|
||||
else:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.guilds.empty():
|
||||
await self.fill_guilds()
|
||||
|
||||
@ -547,13 +530,10 @@ class GuildIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
@ -561,20 +541,6 @@ class GuildIterator(_AsyncIterator):
|
||||
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)
|
||||
@ -611,7 +577,7 @@ class GuildIterator(_AsyncIterator):
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
return data
|
||||
|
||||
class MemberIterator(_AsyncIterator):
|
||||
class MemberIterator(_AsyncIterator['Member']):
|
||||
def __init__(self, guild, limit=1000, after=None):
|
||||
|
||||
if isinstance(after, datetime.datetime):
|
||||
@ -625,7 +591,7 @@ class MemberIterator(_AsyncIterator):
|
||||
self.get_members = self.state.http.get_members
|
||||
self.members = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.members.empty():
|
||||
await self.fill_members()
|
||||
|
||||
@ -636,13 +602,10 @@ class MemberIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 1000:
|
||||
r = 1000
|
||||
elif l <= 1000:
|
||||
r = l
|
||||
else:
|
||||
r = 1000
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import itertools
|
||||
import sys
|
||||
from operator import attrgetter
|
||||
@ -31,6 +31,7 @@ from operator import attrgetter
|
||||
import discord.abc
|
||||
|
||||
from . import utils
|
||||
from .errors import ClientException
|
||||
from .user import BaseUser, User
|
||||
from .activity import create_activity
|
||||
from .permissions import Permissions
|
||||
@ -38,6 +39,11 @@ from .enums import Status, try_enum
|
||||
from .colour import Colour
|
||||
from .object import Object
|
||||
|
||||
__all__ = (
|
||||
'VoiceState',
|
||||
'Member',
|
||||
)
|
||||
|
||||
class VoiceState:
|
||||
"""Represents a Discord user's voice state.
|
||||
|
||||
@ -58,15 +64,32 @@ class VoiceState:
|
||||
|
||||
self_video: :class:`bool`
|
||||
Indicates if the user is currently broadcasting video.
|
||||
suppress: :class:`bool`
|
||||
Indicates if the user is suppressed from speaking.
|
||||
|
||||
Only applies to stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
requested_to_speak_at: Optional[:class:`datetime.datetime`]
|
||||
An aware datetime object that specifies the date and time in UTC that the member
|
||||
requested to speak. It will be ``None`` if they are not requesting to speak
|
||||
anymore or have been accepted to speak.
|
||||
|
||||
Only applicable to stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
afk: :class:`bool`
|
||||
Indicates if the user is currently in the AFK channel in the guild.
|
||||
channel: Optional[:class:`VoiceChannel`]
|
||||
channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]]
|
||||
The voice channel that the user is currently connected to. ``None`` if the user
|
||||
is not currently in a voice channel.
|
||||
"""
|
||||
|
||||
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute',
|
||||
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel')
|
||||
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel',
|
||||
'requested_to_speak_at', 'suppress')
|
||||
|
||||
def __init__(self, *, data, channel=None):
|
||||
self.session_id = data.get('session_id')
|
||||
@ -80,10 +103,21 @@ class VoiceState:
|
||||
self.afk = data.get('suppress', False)
|
||||
self.mute = data.get('mute', False)
|
||||
self.deaf = data.get('deaf', False)
|
||||
self.suppress = data.get('suppress', False)
|
||||
self.requested_to_speak_at = utils.parse_time(data.get('request_to_speak_timestamp'))
|
||||
self.channel = channel
|
||||
|
||||
def __repr__(self):
|
||||
return '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} self_stream={0.self_stream} channel={0.channel!r}>'.format(self)
|
||||
attrs = [
|
||||
('self_mute', self.self_mute),
|
||||
('self_deaf', self.self_deaf),
|
||||
('self_stream', self.self_stream),
|
||||
('suppress', self.suppress),
|
||||
('requested_to_speak_at', self.requested_to_speak_at),
|
||||
('channel', self.channel)
|
||||
]
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {inner}>'
|
||||
|
||||
def flatten_user(cls):
|
||||
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
|
||||
@ -99,21 +133,26 @@ def flatten_user(cls):
|
||||
# slotted members are implemented as member_descriptors in Type.__dict__
|
||||
if not hasattr(value, '__annotations__'):
|
||||
getter = attrgetter('_user.' + attr)
|
||||
setattr(cls, attr, property(getter, doc='Equivalent to :attr:`User.%s`' % attr))
|
||||
setattr(cls, attr, property(getter, doc=f'Equivalent to :attr:`User.{attr}`'))
|
||||
else:
|
||||
# Technically, this can also use attrgetter
|
||||
# However I'm not sure how I feel about "functions" returning properties
|
||||
# It probably breaks something in Sphinx.
|
||||
# probably a member function by now
|
||||
def generate_function(x):
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
# We want sphinx to properly show coroutine functions as coroutines
|
||||
if inspect.iscoroutinefunction(value):
|
||||
async def general(self, *args, **kwargs):
|
||||
return await getattr(self._user, x)(*args, **kwargs)
|
||||
else:
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
|
||||
general.__name__ = x
|
||||
return general
|
||||
|
||||
func = generate_function(attr)
|
||||
func.__doc__ = value.__doc__
|
||||
func = utils.copy_doc(value)(func)
|
||||
setattr(cls, attr, func)
|
||||
|
||||
return cls
|
||||
@ -149,7 +188,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
Attributes
|
||||
----------
|
||||
joined_at: Optional[:class:`datetime.datetime`]
|
||||
A datetime object that specifies the date and time in UTC that the member joined the guild.
|
||||
An aware datetime object that specifies the date and time in UTC that the member joined the guild.
|
||||
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
|
||||
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
|
||||
The activities that the user is currently doing.
|
||||
@ -162,7 +201,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
.. versionadded:: 1.6
|
||||
premium_since: Optional[:class:`datetime.datetime`]
|
||||
A datetime object that specifies the date and time in UTC when the member used their
|
||||
An aware datetime object that specifies the date and time in UTC when the member used their
|
||||
Nitro boost on the guild, if available. This could be ``None``.
|
||||
"""
|
||||
|
||||
@ -190,8 +229,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}' \
|
||||
' bot={1.bot} nick={0.nick!r} guild={0.guild!r}>'.format(self, self._user)
|
||||
return f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}' \
|
||||
f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _BaseUser) and other.id == self.id
|
||||
@ -293,12 +332,12 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
def _update_inner_user(self, user):
|
||||
u = self._user
|
||||
original = (u.name, u.avatar, u.discriminator)
|
||||
original = (u.name, u.avatar, u.discriminator, u._public_flags)
|
||||
# These keys seem to always be available
|
||||
modified = (user['username'], user['avatar'], user['discriminator'])
|
||||
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
|
||||
if original != modified:
|
||||
to_return = User._copy(self._user)
|
||||
u.name, u.avatar, u.discriminator = modified
|
||||
u.name, u.avatar, u.discriminator, u._public_flags = modified
|
||||
# Signal to dispatch on_user_update
|
||||
return to_return, u
|
||||
|
||||
@ -390,8 +429,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention the member."""
|
||||
if self.nick:
|
||||
return '<@!%s>' % self.id
|
||||
return '<@%s>' % self.id
|
||||
return f'<@!{self._user.id}>'
|
||||
return f'<@{self._user.id}>'
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@ -401,7 +440,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if they have a guild specific nickname then that
|
||||
is returned instead.
|
||||
"""
|
||||
return self.nick if self.nick is not None else self.name
|
||||
return self.nick or self.name
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
@ -434,11 +473,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if self._user.mentioned_in(message):
|
||||
return True
|
||||
|
||||
for role in message.role_mentions:
|
||||
if self._roles.has(role.id):
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(self._roles.has(role.id) for role in message.role_mentions)
|
||||
|
||||
def permissions_in(self, channel):
|
||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
||||
@ -560,6 +595,11 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
Indicates if the member should be guild muted or un-muted.
|
||||
deafen: :class:`bool`
|
||||
Indicates if the member should be guild deafened or un-deafened.
|
||||
suppress: :class:`bool`
|
||||
Indicates if the member should be suppressed in stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
roles: Optional[List[:class:`Role`]]
|
||||
The member's new list of roles. This *replaces* the roles.
|
||||
voice_channel: Optional[:class:`VoiceChannel`]
|
||||
@ -577,6 +617,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
"""
|
||||
http = self._state.http
|
||||
guild_id = self.guild.id
|
||||
me = self._state.self_id == self.id
|
||||
payload = {}
|
||||
|
||||
try:
|
||||
@ -585,8 +626,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
# nick not present so...
|
||||
pass
|
||||
else:
|
||||
nick = nick if nick else ''
|
||||
if self._state.self_id == self.id:
|
||||
nick = nick or ''
|
||||
if me:
|
||||
await http.change_my_nickname(guild_id, nick, reason=reason)
|
||||
else:
|
||||
payload['nick'] = nick
|
||||
@ -599,6 +640,23 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if mute is not None:
|
||||
payload['mute'] = mute
|
||||
|
||||
suppress = fields.get('suppress')
|
||||
if suppress is not None:
|
||||
voice_state_payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'suppress': suppress,
|
||||
}
|
||||
|
||||
if suppress or self.bot:
|
||||
voice_state_payload['request_to_speak_timestamp'] = None
|
||||
|
||||
if me:
|
||||
await http.edit_my_voice_state(guild_id, voice_state_payload)
|
||||
else:
|
||||
if not suppress:
|
||||
voice_state_payload['request_to_speak_timestamp'] = datetime.datetime.utcnow().isoformat()
|
||||
await http.edit_voice_state(guild_id, self.id, voice_state_payload)
|
||||
|
||||
try:
|
||||
vc = fields['voice_channel']
|
||||
except KeyError:
|
||||
@ -613,10 +671,43 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
else:
|
||||
payload['roles'] = tuple(r.id for r in roles)
|
||||
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
if payload:
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
|
||||
# TODO: wait for WS event for modify-in-place behaviour
|
||||
|
||||
async def request_to_speak(self):
|
||||
"""|coro|
|
||||
|
||||
Request to speak in the connected channel.
|
||||
|
||||
Only applies to stage channels.
|
||||
|
||||
.. note::
|
||||
|
||||
Requesting members that are not the client is equivalent
|
||||
to :attr:`.edit` providing ``suppress`` as ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have the proper permissions to the action requested.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
"""
|
||||
payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'request_to_speak_timestamp': datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
if self._state.self_id != self.id:
|
||||
payload['suppress'] = False
|
||||
await self._state.http.edit_voice_state(self.guild.id, self.id, payload)
|
||||
else:
|
||||
await self._state.http.edit_my_voice_state(self.guild.id, payload)
|
||||
|
||||
async def move_to(self, channel, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'AllowedMentions',
|
||||
)
|
||||
|
||||
class _FakeBool:
|
||||
def __repr__(self):
|
||||
return 'True'
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -33,7 +31,6 @@ from . import utils
|
||||
from .reaction import Reaction
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .calls import CallMessage
|
||||
from .enums import MessageType, ChannelType, try_enum
|
||||
from .errors import InvalidArgument, ClientException, HTTPException
|
||||
from .embeds import Embed
|
||||
@ -58,7 +55,7 @@ def convert_emoji_reaction(emoji):
|
||||
emoji = emoji.emoji
|
||||
|
||||
if isinstance(emoji, Emoji):
|
||||
return '%s:%s' % (emoji.name, emoji.id)
|
||||
return f'{emoji.name}:{emoji.id}'
|
||||
if isinstance(emoji, PartialEmoji):
|
||||
return emoji._as_reaction()
|
||||
if isinstance(emoji, str):
|
||||
@ -66,11 +63,32 @@ def convert_emoji_reaction(emoji):
|
||||
# No existing emojis have <> in them, so this should be okay.
|
||||
return emoji.strip('<>')
|
||||
|
||||
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
|
||||
raise InvalidArgument(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.')
|
||||
|
||||
class Attachment:
|
||||
class Attachment(Hashable):
|
||||
"""Represents an attachment from Discord.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the URL of the attachment.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the attachment is equal to another attachment.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the attachment is not equal to another attachment.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the attachment.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Attachment can now be casted to :class:`str` and is hashable.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
id: :class:`int`
|
||||
@ -90,9 +108,13 @@ class Attachment:
|
||||
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
|
||||
case of images. When the message is deleted, this URL might be valid for a few
|
||||
minutes or not valid at all.
|
||||
content_type: Optional[:class:`str`]
|
||||
The attachment's `media type <https://en.wikipedia.org/wiki/Media_type>`_
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http')
|
||||
__slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http', 'content_type')
|
||||
|
||||
def __init__(self, *, data, state):
|
||||
self.id = int(data['id'])
|
||||
@ -103,6 +125,7 @@ class Attachment:
|
||||
self.url = data.get('url')
|
||||
self.proxy_url = data.get('proxy_url')
|
||||
self._http = state.http
|
||||
self.content_type = data.get('content_type')
|
||||
|
||||
def is_spoiler(self):
|
||||
""":class:`bool`: Whether this attachment contains a spoiler."""
|
||||
@ -114,6 +137,9 @@ class Attachment:
|
||||
def __repr__(self):
|
||||
return '<Attachment id={0.id} filename={0.filename!r} url={0.url!r}>'.format(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.url or ''
|
||||
|
||||
async def save(self, fp, *, seek_begin=True, use_cached=False):
|
||||
"""|coro|
|
||||
|
||||
@ -282,6 +308,12 @@ class MessageReference:
|
||||
The channel id of the message referenced.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild id of the message referenced.
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying to the referenced message should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
resolved: Optional[Union[:class:`Message`, :class:`DeletedReferencedMessage`]]
|
||||
The message that this reference resolved to. If this is ``None``
|
||||
then the original message was not fetched either due to the Discord API
|
||||
@ -294,14 +326,15 @@ class MessageReference:
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'resolved', '_state')
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state')
|
||||
|
||||
def __init__(self, *, message_id, channel_id, guild_id=None):
|
||||
def __init__(self, *, message_id, channel_id, guild_id=None, fail_if_not_exists=True):
|
||||
self._state = None
|
||||
self.resolved = None
|
||||
self.message_id = message_id
|
||||
self.channel_id = channel_id
|
||||
self.guild_id = guild_id
|
||||
self.fail_if_not_exists = fail_if_not_exists
|
||||
|
||||
@classmethod
|
||||
def with_state(cls, state, data):
|
||||
@ -314,7 +347,7 @@ class MessageReference:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message):
|
||||
def from_message(cls, message, *, fail_if_not_exists=True):
|
||||
"""Creates a :class:`MessageReference` from an existing :class:`~discord.Message`.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
@ -323,13 +356,18 @@ class MessageReference:
|
||||
----------
|
||||
message: :class:`~discord.Message`
|
||||
The message to be converted into a reference.
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying to the referenced message should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`MessageReference`
|
||||
A reference to the message.
|
||||
"""
|
||||
self = cls(message_id=message.id, channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None))
|
||||
self = cls(message_id=message.id, channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None), fail_if_not_exists=fail_if_not_exists)
|
||||
self._state = message._state
|
||||
return self
|
||||
|
||||
@ -356,6 +394,15 @@ class MessageReference:
|
||||
"""Optional[:class:`~discord.Message`]: The cached message, if found in the internal message cache."""
|
||||
return self._state._get_message(self.message_id)
|
||||
|
||||
@property
|
||||
def jump_url(self):
|
||||
""":class:`str`: Returns a URL that allows the client to jump to the referenced message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
guild_id = self.guild_id if self.guild_id is not None else '@me'
|
||||
return 'https://discord.com/channels/{0}/{1.channel_id}/{1.message_id}'.format(guild_id, self)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MessageReference message_id={0.message_id!r} channel_id={0.channel_id!r} guild_id={0.guild_id!r}>'.format(self)
|
||||
|
||||
@ -364,6 +411,8 @@ class MessageReference:
|
||||
result['channel_id'] = self.channel_id
|
||||
if self.guild_id is not None:
|
||||
result['guild_id'] = self.guild_id
|
||||
if self.fail_if_not_exists is not None:
|
||||
result['fail_if_not_exists'] = self.fail_if_not_exists
|
||||
return result
|
||||
|
||||
to_message_reference_dict = to_dict
|
||||
@ -418,15 +467,12 @@ class Message(Hashable):
|
||||
The actual contents of the message.
|
||||
nonce
|
||||
The value used by the discord guild and the client to verify that the message is successfully sent.
|
||||
This is typically non-important.
|
||||
This is not stored long term within Discord's servers and is only used ephemerally.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds the message has.
|
||||
channel: Union[:class:`abc.Messageable`]
|
||||
The :class:`TextChannel` that the message was sent from.
|
||||
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
|
||||
call: Optional[:class:`CallMessage`]
|
||||
The call that the message refers to. This is only applicable to messages of type
|
||||
:attr:`MessageType.call`.
|
||||
reference: Optional[:class:`~discord.MessageReference`]
|
||||
The message that this message references. This is only applicable to messages of
|
||||
type :attr:`MessageType.pins_add`, crossposted messages created by a
|
||||
@ -502,7 +548,7 @@ class Message(Hashable):
|
||||
'mention_everyone', 'embeds', 'id', 'mentions', 'author',
|
||||
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
|
||||
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
|
||||
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags',
|
||||
'role_mentions', '_cs_raw_role_mentions', 'type', 'flags',
|
||||
'_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference',
|
||||
'application', 'activity', 'stickers')
|
||||
|
||||
@ -548,9 +594,9 @@ class Message(Hashable):
|
||||
|
||||
ref.resolved = self.__class__(channel=chan, data=resolved, state=state)
|
||||
|
||||
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
|
||||
for handler in ('author', 'member', 'mentions', 'mention_roles', 'flags'):
|
||||
try:
|
||||
getattr(self, '_handle_%s' % handler)(data[handler])
|
||||
getattr(self, f'_handle_{handler}')(data[handler])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
@ -722,26 +768,6 @@ class Message(Hashable):
|
||||
if role is not None:
|
||||
self.role_mentions.append(role)
|
||||
|
||||
def _handle_call(self, call):
|
||||
if call is None or self.type is not MessageType.call:
|
||||
self.call = None
|
||||
return
|
||||
|
||||
# we get the participant source from the mentions array or
|
||||
# the author
|
||||
|
||||
participants = []
|
||||
for uid in map(int, call.get('participants', [])):
|
||||
if uid == self.author.id:
|
||||
participants.append(self.author)
|
||||
else:
|
||||
user = utils.find(lambda u: u.id == uid, self.mentions)
|
||||
if user is not None:
|
||||
participants.append(user)
|
||||
|
||||
call['participants'] = participants
|
||||
self.call = CallMessage(message=self, **call)
|
||||
|
||||
def _rebind_channel_reference(self, new_channel):
|
||||
self.channel = new_channel
|
||||
|
||||
@ -798,24 +824,24 @@ class Message(Hashable):
|
||||
|
||||
.. note::
|
||||
|
||||
This *does not* escape markdown. If you want to escape
|
||||
markdown then use :func:`utils.escape_markdown` along
|
||||
with this function.
|
||||
This *does not* affect markdown. If you want to escape
|
||||
or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown`
|
||||
respectively, along with this function.
|
||||
"""
|
||||
|
||||
transformations = {
|
||||
re.escape('<#%s>' % channel.id): '#' + channel.name
|
||||
re.escape(f'<#{channel.id}>'): '#' + channel.name
|
||||
for channel in self.channel_mentions
|
||||
}
|
||||
|
||||
mention_transforms = {
|
||||
re.escape('<@%s>' % member.id): '@' + member.display_name
|
||||
re.escape(f'<@{member.id}>'): '@' + member.display_name
|
||||
for member in self.mentions
|
||||
}
|
||||
|
||||
# add the <@!user_id> cases as well..
|
||||
second_mention_transforms = {
|
||||
re.escape('<@!%s>' % member.id): '@' + member.display_name
|
||||
re.escape(f'<@!{member.id}>'): '@' + member.display_name
|
||||
for member in self.mentions
|
||||
}
|
||||
|
||||
@ -824,7 +850,7 @@ class Message(Hashable):
|
||||
|
||||
if self.guild is not None:
|
||||
role_transforms = {
|
||||
re.escape('<@&%s>' % role.id): '@' + role.name
|
||||
re.escape(f'<@&{role.id}>'): '@' + role.name
|
||||
for role in self.role_mentions
|
||||
}
|
||||
transformations.update(role_transforms)
|
||||
@ -843,7 +869,7 @@ class Message(Hashable):
|
||||
|
||||
@property
|
||||
def edited_at(self):
|
||||
"""Optional[:class:`datetime.datetime`]: A naive UTC datetime object containing the edited time of the message."""
|
||||
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message."""
|
||||
return self._edited_timestamp
|
||||
|
||||
@property
|
||||
@ -873,7 +899,7 @@ class Message(Hashable):
|
||||
return self.content
|
||||
|
||||
if self.type is MessageType.pins_add:
|
||||
return '{0.name} pinned a message to this channel.'.format(self.author)
|
||||
return f'{self.author.name} pinned a message to this channel.'
|
||||
|
||||
if self.type is MessageType.recipient_add:
|
||||
return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0])
|
||||
@ -885,7 +911,7 @@ class Message(Hashable):
|
||||
return '{0.author.name} changed the channel name: {0.content}'.format(self)
|
||||
|
||||
if self.type is MessageType.channel_icon_change:
|
||||
return '{0.author.name} changed the channel icon.'.format(self)
|
||||
return f'{self.author.name} changed the channel icon.'
|
||||
|
||||
if self.type is MessageType.new_member:
|
||||
formats = [
|
||||
@ -904,27 +930,11 @@ class Message(Hashable):
|
||||
"Yay you made it, {0}!",
|
||||
]
|
||||
|
||||
# manually reconstruct the epoch with millisecond precision, because
|
||||
# datetime.datetime.timestamp() doesn't return the exact posix
|
||||
# timestamp with the precision that we need
|
||||
created_at_ms = int((self.created_at - datetime.datetime(1970, 1, 1)).total_seconds() * 1000)
|
||||
created_at_ms = int(self.created_at.timestamp() * 1000)
|
||||
return formats[created_at_ms % len(formats)].format(self.author.name)
|
||||
|
||||
if self.type is MessageType.call:
|
||||
# we're at the call message type now, which is a bit more complicated.
|
||||
# we can make the assumption that Message.channel is a PrivateChannel
|
||||
# with the type ChannelType.group or ChannelType.private
|
||||
call_ended = self.call.ended_timestamp is not None
|
||||
|
||||
if self.channel.me in self.call.participants:
|
||||
return '{0.author.name} started a call.'.format(self)
|
||||
elif call_ended:
|
||||
return 'You missed a call from {0.author.name}'.format(self)
|
||||
else:
|
||||
return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self)
|
||||
|
||||
if self.type is MessageType.premium_guild_subscription:
|
||||
return '{0.author.name} just boosted the server!'.format(self)
|
||||
return f'{self.author.name} just boosted the server!'
|
||||
|
||||
if self.type is MessageType.premium_guild_tier_1:
|
||||
return '{0.author.name} just boosted the server! {0.guild} has achieved **Level 1!**'.format(self)
|
||||
@ -938,7 +948,22 @@ class Message(Hashable):
|
||||
if self.type is MessageType.channel_follow_add:
|
||||
return '{0.author.name} has added {0.content} to this channel'.format(self)
|
||||
|
||||
async def delete(self, *, delay=None, silent=False):
|
||||
if self.type is MessageType.guild_stream:
|
||||
return '{0.author.name} is live! Now streaming {0.author.activity.name}'.format(self)
|
||||
|
||||
if self.type is MessageType.guild_discovery_disqualified:
|
||||
return 'This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'
|
||||
|
||||
if self.type is MessageType.guild_discovery_requalified:
|
||||
return 'This server is eligible for Server Discovery again and has been automatically relisted!'
|
||||
|
||||
if self.type is MessageType.guild_discovery_grace_period_initial_warning:
|
||||
return 'This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'
|
||||
|
||||
if self.type is MessageType.guild_discovery_grace_period_final_warning:
|
||||
return 'This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'
|
||||
|
||||
async def delete(self, *, delay=None):
|
||||
"""|coro|
|
||||
|
||||
Deletes the message.
|
||||
@ -978,7 +1003,7 @@ class Message(Hashable):
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(delete(), loop=self._state.loop)
|
||||
asyncio.create_task(delete())
|
||||
else:
|
||||
try:
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
@ -1272,26 +1297,6 @@ class Message(Hashable):
|
||||
"""
|
||||
await self._state.http.clear_reactions(self.channel.id, self.id)
|
||||
|
||||
async def ack(self):
|
||||
"""|coro|
|
||||
|
||||
Marks this message as read.
|
||||
|
||||
The user must not be a bot user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Acking failed.
|
||||
ClientException
|
||||
You must not be a bot user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
if state.is_bot:
|
||||
raise ClientException('Must not be a bot account to ack messages.')
|
||||
return await state.http.ack_message(self.channel.id, self.id)
|
||||
|
||||
async def reply(self, content=None, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
@ -1318,18 +1323,26 @@ class Message(Hashable):
|
||||
|
||||
return await self.channel.send(content, reference=self, **kwargs)
|
||||
|
||||
def to_reference(self):
|
||||
def to_reference(self, *, fail_if_not_exists=True):
|
||||
"""Creates a :class:`~discord.MessageReference` from the current message.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying using the message reference should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`~discord.MessageReference`
|
||||
The reference to this message.
|
||||
"""
|
||||
|
||||
return MessageReference.from_message(self)
|
||||
return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists)
|
||||
|
||||
def to_message_reference_dict(self):
|
||||
data = {
|
||||
@ -1389,7 +1402,6 @@ class PartialMessage(Hashable):
|
||||
_exported_names = (
|
||||
'jump_url',
|
||||
'delete',
|
||||
'edit',
|
||||
'publish',
|
||||
'pin',
|
||||
'unpin',
|
||||
@ -1404,7 +1416,7 @@ class PartialMessage(Hashable):
|
||||
|
||||
def __init__(self, *, channel, id):
|
||||
if channel.type not in (ChannelType.text, ChannelType.news, ChannelType.private):
|
||||
raise TypeError('Expected TextChannel or DMChannel not %r' % type(channel))
|
||||
raise TypeError(f'Expected TextChannel or DMChannel not {type(channel)!r}')
|
||||
|
||||
self.channel = channel
|
||||
self._state = channel._state
|
||||
@ -1454,3 +1466,102 @@ class PartialMessage(Hashable):
|
||||
|
||||
data = await self._state.http.get_message(self.channel.id, self.id)
|
||||
return self._state.create_message(channel=self.channel, data=data)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the message.
|
||||
|
||||
The content must be able to be transformed into a string via ``str(content)``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
:class:`discord.Message` is returned instead of ``None`` if an edit took place.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: Optional[:class:`str`]
|
||||
The new content to replace the message with.
|
||||
Could be ``None`` to remove the content.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The new embed to replace the original with.
|
||||
Could be ``None`` to remove the embed.
|
||||
suppress: :class:`bool`
|
||||
Whether to suppress embeds for the message. This removes
|
||||
all the embeds if set to ``True``. If set to ``False``
|
||||
this brings the embeds back if they were suppressed.
|
||||
Using this parameter requires :attr:`~.Permissions.manage_messages`.
|
||||
delete_after: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait in the background
|
||||
before deleting the message we just edited. If the deletion fails,
|
||||
then it is silently ignored.
|
||||
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
|
||||
Controls the mentions being processed in this message. If this is
|
||||
passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
|
||||
The merging behaviour only overrides attributes that have been explicitly passed
|
||||
to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
|
||||
If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
|
||||
are used instead.
|
||||
|
||||
Raises
|
||||
-------
|
||||
NotFound
|
||||
The message was not found.
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
Forbidden
|
||||
Tried to suppress a message without permissions or
|
||||
edited a message's content or embed that isn't yours.
|
||||
|
||||
Returns
|
||||
---------
|
||||
Optional[:class:`Message`]
|
||||
The message that was edited.
|
||||
"""
|
||||
|
||||
try:
|
||||
content = fields['content']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if content is not None:
|
||||
fields['content'] = str(content)
|
||||
|
||||
try:
|
||||
embed = fields['embed']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if embed is not None:
|
||||
fields['embed'] = embed.to_dict()
|
||||
|
||||
try:
|
||||
suppress = fields.pop('suppress')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
flags = MessageFlags._from_value(0)
|
||||
flags.suppress_embeds = suppress
|
||||
fields['flags'] = flags.value
|
||||
|
||||
delete_after = fields.pop('delete_after', None)
|
||||
|
||||
try:
|
||||
allowed_mentions = fields.pop('allowed_mentions')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if allowed_mentions is not None:
|
||||
if self._state.allowed_mentions is not None:
|
||||
allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions).to_dict()
|
||||
else:
|
||||
allowed_mentions = allowed_mentions.to_dict()
|
||||
fields['allowed_mentions'] = allowed_mentions
|
||||
|
||||
if fields:
|
||||
data = await self._state.http.edit_message(self.channel.id, self.id, **fields)
|
||||
|
||||
if delete_after is not None:
|
||||
await self.delete(delay=delete_after)
|
||||
|
||||
if fields:
|
||||
return self._state.create_message(channel=self.channel, data=data)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,6 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'EqualityComparable',
|
||||
'Hashable',
|
||||
)
|
||||
|
||||
class EqualityComparable:
|
||||
__slots__ = ()
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
from . import utils
|
||||
from .mixins import Hashable
|
||||
|
||||
__all__ = (
|
||||
'Object',
|
||||
)
|
||||
|
||||
class Object(Hashable):
|
||||
"""Represents a generic Discord object.
|
||||
|
||||
@ -65,12 +67,12 @@ class Object(Hashable):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
raise TypeError('id parameter must be convertable to int not {0.__class__!r}'.format(id)) from None
|
||||
raise TypeError(f'id parameter must be convertable to int not {id.__class__!r}') from None
|
||||
else:
|
||||
self.id = id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Object id=%r>' % self.id
|
||||
return f'<Object id={self.id!r}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -28,6 +26,12 @@ import struct
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'OggError',
|
||||
'OggPage',
|
||||
'OggStream',
|
||||
)
|
||||
|
||||
class OggError(DiscordException):
|
||||
"""An exception that is thrown for Ogg stream parsing errors."""
|
||||
pass
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -35,6 +33,12 @@ import sys
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'Encoder',
|
||||
'OpusError',
|
||||
'OpusNotLoaded',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
@ -185,7 +189,7 @@ def _load_default():
|
||||
_basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
_bitness = struct.calcsize('P') * 8
|
||||
_target = 'x64' if _bitness > 32 else 'x86'
|
||||
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target))
|
||||
_filename = os.path.join(_basedir, 'bin', f'libopus-0.{_target}.dll')
|
||||
_lib = libopus_loader(_filename)
|
||||
else:
|
||||
_lib = libopus_loader(ctypes.util.find_library('opus'))
|
||||
@ -276,17 +280,14 @@ class _OpusStruct:
|
||||
|
||||
@staticmethod
|
||||
def get_opus_version() -> str:
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
if not is_loaded() and not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
return _lib.opus_get_version_string().decode('utf-8')
|
||||
|
||||
class Encoder(_OpusStruct):
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self.application = application
|
||||
self._state = self._create_state()
|
||||
@ -313,14 +314,14 @@ class Encoder(_OpusStruct):
|
||||
|
||||
def set_bandwidth(self, req):
|
||||
if req not in band_ctl:
|
||||
raise KeyError('%r is not a valid bandwidth setting. Try one of: %s' % (req, ','.join(band_ctl)))
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}')
|
||||
|
||||
k = band_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
|
||||
|
||||
def set_signal_type(self, req):
|
||||
if req not in signal_ctl:
|
||||
raise KeyError('%r is not a valid signal setting. Try one of: %s' % (req, ','.join(signal_ctl)))
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
|
||||
|
||||
k = signal_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
|
||||
@ -342,9 +343,7 @@ class Encoder(_OpusStruct):
|
||||
|
||||
class Decoder(_OpusStruct):
|
||||
def __init__(self):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self._state = self._create_state()
|
||||
|
||||
@ -374,7 +373,7 @@ class Decoder(_OpusStruct):
|
||||
|
||||
def _set_gain(self, adjustment):
|
||||
"""Configures decoder gain adjustment.
|
||||
|
||||
|
||||
Scales the decoded output by a factor specified in Q8 dB units.
|
||||
This has a maximum range of -32768 to 32767 inclusive, and returns
|
||||
OPUS_BAD_ARG (-1) otherwise. The default is zero indicating no adjustment.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,6 +25,9 @@ DEALINGS IN THE SOFTWARE.
|
||||
from .asset import Asset
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'PartialEmoji',
|
||||
)
|
||||
|
||||
class _EmojiTag:
|
||||
__slots__ = ()
|
||||
@ -103,8 +104,8 @@ class PartialEmoji(_EmojiTag):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
if self.animated:
|
||||
return '<a:%s:%s>' % (self.name, self.id)
|
||||
return '<:%s:%s>' % (self.name, self.id)
|
||||
return f'<a:{self.name}:{self.id}>'
|
||||
return f'<:{self.name}:{self.id}>'
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
|
||||
@ -134,7 +135,7 @@ class PartialEmoji(_EmojiTag):
|
||||
def _as_reaction(self):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
return '%s:%s' % (self.name, self.id)
|
||||
return f'{self.name}:{self.id}'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -149,10 +150,43 @@ class PartialEmoji(_EmojiTag):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`Asset`: Returns an asset of the emoji, if it is custom."""
|
||||
""":class:`Asset`: Returns the asset of the emoji, if it is custom.
|
||||
|
||||
This is equivalent to calling :meth:`url_as` with
|
||||
the default parameters (i.e. png/gif detection).
|
||||
"""
|
||||
return self.url_as(format=None)
|
||||
|
||||
def url_as(self, *, format=None, static_format="png"):
|
||||
"""Returns an :class:`Asset` for the emoji's url, if it is custom.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
||||
'gif' is only valid for animated emojis.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
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.
|
||||
"""
|
||||
if self.is_unicode_emoji():
|
||||
return Asset(self._state)
|
||||
|
||||
_format = 'gif' if self.animated else 'png'
|
||||
url = "/emojis/{0.id}.{1}".format(self, _format)
|
||||
return Asset(self._state, url)
|
||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -96,12 +94,12 @@ class Permissions(BaseFlags):
|
||||
|
||||
def __init__(self, permissions=0, **kwargs):
|
||||
if not isinstance(permissions, int):
|
||||
raise TypeError('Expected int parameter, received %s instead.' % permissions.__class__.__name__)
|
||||
raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
|
||||
|
||||
self.value = permissions
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid permission name.' % key)
|
||||
raise TypeError(f'{key!r} is not a valid permission name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
def is_subset(self, other):
|
||||
@ -109,14 +107,14 @@ class Permissions(BaseFlags):
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value & other.value) == self.value
|
||||
else:
|
||||
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__))
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_superset(self, other):
|
||||
"""Returns ``True`` if self has the same or more permissions as other."""
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value | other.value) == self.value
|
||||
else:
|
||||
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__))
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_strict_subset(self, other):
|
||||
"""Returns ``True`` if the permissions on other are a strict subset of those on self."""
|
||||
@ -140,8 +138,9 @@ class Permissions(BaseFlags):
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to ``True``."""
|
||||
return cls(0b01111111111111111111111111111111)
|
||||
permissions set to ``True``.
|
||||
"""
|
||||
return cls(0b111111111111111111111111111111111)
|
||||
|
||||
@classmethod
|
||||
def all_channel(cls):
|
||||
@ -149,26 +148,53 @@ class Permissions(BaseFlags):
|
||||
``True`` and the guild-specific ones set to ``False``. The guild-specific
|
||||
permissions are currently:
|
||||
|
||||
- manage_guild
|
||||
- kick_members
|
||||
- ban_members
|
||||
- administrator
|
||||
- change_nickname
|
||||
- manage_nicknames
|
||||
- :attr:`manage_emojis`
|
||||
- :attr:`view_audit_log`
|
||||
- :attr:`view_guild_insights`
|
||||
- :attr:`manage_guild`
|
||||
- :attr:`change_nickname`
|
||||
- :attr:`manage_nicknames`
|
||||
- :attr:`kick_members`
|
||||
- :attr:`ban_members`
|
||||
- :attr:`administrator`
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_slash_commands` permissions.
|
||||
"""
|
||||
return cls(0b00110011111101111111110001010001)
|
||||
return cls(0b10110011111101111111111101010001)
|
||||
|
||||
@classmethod
|
||||
def general(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"General" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b01111100000010000000000010111111)
|
||||
"General" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Permission :attr:`read_messages` is now included in the general permissions, but
|
||||
permissions :attr:`administrator`, :attr:`create_instant_invite`, :attr:`kick_members`,
|
||||
:attr:`ban_members`, :attr:`change_nickname` and :attr:`manage_nicknames` are
|
||||
no longer part of the general permissions.
|
||||
"""
|
||||
return cls(0b01110000000010000000010010110000)
|
||||
|
||||
@classmethod
|
||||
def membership(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Membership" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(0b00001100000000000000000000000111)
|
||||
|
||||
@classmethod
|
||||
def text(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Text" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b00000000000001111111110001000000)
|
||||
"Text" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Permission :attr:`read_messages` is no longer part of the text permissions.
|
||||
Added :attr:`use_slash_commands` permission.
|
||||
"""
|
||||
return cls(0b10000000000001111111100001000000)
|
||||
|
||||
@classmethod
|
||||
def voice(cls):
|
||||
@ -176,6 +202,32 @@ class Permissions(BaseFlags):
|
||||
"Voice" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b00000011111100000000001100000000)
|
||||
|
||||
@classmethod
|
||||
def stage(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Channel" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(1 << 32)
|
||||
|
||||
@classmethod
|
||||
def stage_moderator(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Moderator" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(0b100000001010000000000000000000000)
|
||||
|
||||
@classmethod
|
||||
def advanced(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Advanced" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(1 << 3)
|
||||
|
||||
def update(self, **kwargs):
|
||||
r"""Bulk updates this permission object.
|
||||
@ -405,9 +457,21 @@ class Permissions(BaseFlags):
|
||||
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis."""
|
||||
return 1 << 30
|
||||
|
||||
# 1 unused
|
||||
@flag_value
|
||||
def use_slash_commands(self):
|
||||
""":class:`bool`: Returns ``True`` if a user can use slash commands.
|
||||
|
||||
# after these 32 bits, there's 21 more unused ones technically
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return 1 << 31
|
||||
|
||||
@flag_value
|
||||
def request_to_speak(self):
|
||||
""":class:`bool`: Returns ``True`` if a user can request to speak in a stage channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return 1 << 32
|
||||
|
||||
def augment_from_permissions(cls):
|
||||
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
|
||||
@ -475,7 +539,7 @@ class PermissionOverwrite:
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_NAMES:
|
||||
raise ValueError('no permission called {0}.'.format(key))
|
||||
raise ValueError(f'no permission called {key}.')
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
@ -484,7 +548,7 @@ class PermissionOverwrite:
|
||||
|
||||
def _set(self, key, value):
|
||||
if value not in (True, None, False):
|
||||
raise TypeError('Expected bool or NoneType, received {0.__class__.__name__}'.format(value))
|
||||
raise TypeError(f'Expected bool or NoneType, received {value.__class__.__name__}')
|
||||
|
||||
if value is None:
|
||||
self._values.pop(key, None)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -313,7 +311,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
'-c:a', codec,
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
'-b:a', '%sk' % bitrate,
|
||||
'-b:a', f'{bitrate}k',
|
||||
'-loglevel', 'warning'))
|
||||
|
||||
if isinstance(options, str):
|
||||
@ -421,7 +419,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
if isinstance(method, str):
|
||||
probefunc = getattr(cls, '_probe_codec_' + method, None)
|
||||
if probefunc is None:
|
||||
raise AttributeError("Invalid probe method '%s'" % method)
|
||||
raise AttributeError(f"Invalid probe method {method!r}")
|
||||
|
||||
if probefunc is cls._probe_codec_native:
|
||||
fallback = cls._probe_codec_fallback
|
||||
@ -431,7 +429,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
fallback = cls._probe_codec_fallback
|
||||
else:
|
||||
raise TypeError("Expected str or callable for parameter 'probe', " \
|
||||
"not '{0.__class__.__name__}'" .format(method))
|
||||
f"not '{method.__class__.__name__}'")
|
||||
|
||||
codec = bitrate = None
|
||||
loop = asyncio.get_event_loop()
|
||||
@ -519,7 +517,7 @@ class PCMVolumeTransformer(AudioSource):
|
||||
|
||||
def __init__(self, original, volume=1.0):
|
||||
if not isinstance(original, AudioSource):
|
||||
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(original))
|
||||
raise TypeError(f'expected AudioSource not {original.__class__.__name__}.')
|
||||
|
||||
if original.is_opus():
|
||||
raise ClientException('AudioSource must not be Opus encoded.')
|
||||
@ -619,7 +617,7 @@ class AudioPlayer(threading.Thread):
|
||||
exc.__context__ = error
|
||||
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
||||
elif error:
|
||||
msg = 'Exception in voice thread {}'.format(self.name)
|
||||
msg = f'Exception in voice thread {self.name}'
|
||||
log.exception(msg, exc_info=error)
|
||||
print(msg, file=sys.stderr)
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,10 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'RawMessageDeleteEvent',
|
||||
'RawBulkMessageDeleteEvent',
|
||||
'RawMessageUpdateEvent',
|
||||
'RawReactionActionEvent',
|
||||
'RawReactionClearEvent',
|
||||
'RawReactionClearEmojiEvent',
|
||||
)
|
||||
|
||||
class _RawReprMixin:
|
||||
def __repr__(self):
|
||||
value = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in self.__slots__)
|
||||
return '<%s %s>' % (self.__class__.__name__, value)
|
||||
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
|
||||
return f'<{self.__class__.__name__} {value}>'
|
||||
|
||||
class RawMessageDeleteEvent(_RawReprMixin):
|
||||
"""Represents the event payload for a :func:`on_raw_message_delete` event.
|
||||
@ -93,14 +100,19 @@ class RawMessageUpdateEvent(_RawReprMixin):
|
||||
The channel ID where the update took place.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the message got updated, if applicable.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
data: :class:`dict`
|
||||
The raw data given by the `gateway <https://discord.com/developers/docs/topics/gateway#message-update>`_
|
||||
cached_message: Optional[:class:`Message`]
|
||||
The cached message, if found in the internal message cache.
|
||||
The cached message, if found in the internal message cache. Represents the message before
|
||||
it is modified by the data in :attr:`RawMessageUpdateEvent.data`.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'data', 'cached_message')
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message')
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data['id'])
|
||||
@ -108,6 +120,11 @@ class RawMessageUpdateEvent(_RawReprMixin):
|
||||
self.data = data
|
||||
self.cached_message = None
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
|
||||
class RawReactionActionEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_reaction_add` or
|
||||
:func:`on_raw_reaction_remove` event.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from .iterators import ReactionIterator
|
||||
|
||||
__all__ = (
|
||||
'Reaction',
|
||||
)
|
||||
|
||||
class Reaction:
|
||||
"""Represents a reaction to a message.
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 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 RelationshipType, try_enum
|
||||
|
||||
class Relationship:
|
||||
"""Represents a relationship in Discord.
|
||||
|
||||
A relationship is like a friendship, a person who is blocked, etc.
|
||||
Only non-bot accounts can have relationships.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
user: :class:`User`
|
||||
The user you have the relationship with.
|
||||
type: :class:`RelationshipType`
|
||||
The type of relationship you have.
|
||||
"""
|
||||
|
||||
__slots__ = ('type', 'user', '_state')
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.type = try_enum(RelationshipType, data['type'])
|
||||
self.user = state.store_user(data['user'])
|
||||
|
||||
def __repr__(self):
|
||||
return '<Relationship user={0.user!r} type={0.type!r}>'.format(self)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the relationship.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Deleting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.remove_relationship(self.user.id)
|
||||
|
||||
async def accept(self):
|
||||
"""|coro|
|
||||
|
||||
Accepts the relationship request. e.g. accepting a
|
||||
friend request.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Accepting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.user.id)
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -30,6 +28,11 @@ from .colour import Colour
|
||||
from .mixins import Hashable
|
||||
from .utils import snowflake_time, _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
'RoleTags',
|
||||
'Role',
|
||||
)
|
||||
|
||||
class RoleTags:
|
||||
"""Represents tags on a role.
|
||||
|
||||
@ -191,7 +194,7 @@ class Role(Hashable):
|
||||
|
||||
def _update(self, data):
|
||||
self.name = data['name']
|
||||
self._permissions = data.get('permissions', 0)
|
||||
self._permissions = int(data.get('permissions_new', 0))
|
||||
self.position = data.get('position', 0)
|
||||
self._colour = data.get('color', 0)
|
||||
self.hoist = data.get('hoist', False)
|
||||
@ -251,7 +254,7 @@ class Role(Hashable):
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention a role."""
|
||||
return '<@&%s>' % self.id
|
||||
return f'<@&{self.id}>'
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
@ -343,7 +346,7 @@ class Role(Hashable):
|
||||
|
||||
payload = {
|
||||
'name': fields.get('name', self.name),
|
||||
'permissions': fields.get('permissions', self.permissions).value,
|
||||
'permissions': str(fields.get('permissions', self.permissions).value),
|
||||
'color': colour.value,
|
||||
'hoist': fields.get('hoist', self.hoist),
|
||||
'mentionable': fields.get('mentionable', self.mentionable)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -46,6 +44,11 @@ from .errors import (
|
||||
from . import utils
|
||||
from .enums import Status
|
||||
|
||||
__all__ = (
|
||||
'AutoShardedClient',
|
||||
'ShardInfo',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventType:
|
||||
@ -319,7 +322,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
def _get_state(self, **options):
|
||||
return AutoShardedConnectionState(dispatch=self.dispatch,
|
||||
handlers=self._handlers, syncer=self._syncer,
|
||||
handlers=self._handlers,
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
|
||||
@property
|
||||
@ -413,7 +416,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
self._connection.shard_count = self.shard_count
|
||||
|
||||
shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count)
|
||||
shard_ids = self.shard_ids or range(self.shard_count)
|
||||
self._connection.shard_ids = shard_ids
|
||||
|
||||
for shard_id in shard_ids:
|
||||
|
177
discord/state.py
177
discord/state.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -44,7 +42,6 @@ from .emoji import Emoji
|
||||
from .mentions import AllowedMentions
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .message import Message
|
||||
from .relationship import Relationship
|
||||
from .channel import *
|
||||
from .raw_models import *
|
||||
from .member import Member
|
||||
@ -54,6 +51,7 @@ from . import utils
|
||||
from .flags import Intents, MemberCacheFlags
|
||||
from .object import Object
|
||||
from .invite import Invite
|
||||
from .interactions import Interaction
|
||||
|
||||
class ChunkRequest:
|
||||
def __init__(self, guild_id, loop, resolver, *, cache=True):
|
||||
@ -104,7 +102,7 @@ async def logging_coroutine(coroutine, *, info):
|
||||
log.exception('Exception occurred during %s', info)
|
||||
|
||||
class ConnectionState:
|
||||
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options):
|
||||
def __init__(self, *, dispatch, handlers, hooks, http, loop, **options):
|
||||
self.loop = loop
|
||||
self.http = http
|
||||
self.max_messages = options.get('max_messages', 1000)
|
||||
@ -112,12 +110,11 @@ class ConnectionState:
|
||||
self.max_messages = 1000
|
||||
|
||||
self.dispatch = dispatch
|
||||
self.syncer = syncer
|
||||
self.is_bot = None
|
||||
self.handlers = handlers
|
||||
self.hooks = hooks
|
||||
self.shard_count = None
|
||||
self._ready_task = None
|
||||
self.application_id = utils._get_as_snowflake(options, 'application_id')
|
||||
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
|
||||
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
|
||||
if self.guild_ready_timeout < 0:
|
||||
@ -149,7 +146,7 @@ class ConnectionState:
|
||||
intents = options.get('intents', None)
|
||||
if intents is not None:
|
||||
if not isinstance(intents, Intents):
|
||||
raise TypeError('intents parameter must be Intent not %r' % type(intents))
|
||||
raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
|
||||
else:
|
||||
intents = Intents.default()
|
||||
|
||||
@ -175,7 +172,7 @@ class ConnectionState:
|
||||
cache_flags = MemberCacheFlags.from_intents(intents)
|
||||
else:
|
||||
if not isinstance(cache_flags, MemberCacheFlags):
|
||||
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags))
|
||||
raise TypeError(f'member_cache_flags parameter must be MemberCacheFlags not {type(cache_flags)!r}')
|
||||
|
||||
cache_flags._verify_intents(intents)
|
||||
|
||||
@ -198,7 +195,6 @@ class ConnectionState:
|
||||
self.user = None
|
||||
self._users = weakref.WeakValueDictionary()
|
||||
self._emojis = {}
|
||||
self._calls = {}
|
||||
self._guilds = {}
|
||||
self._voice_clients = {}
|
||||
|
||||
@ -340,7 +336,7 @@ class ConnectionState:
|
||||
channel_id = channel.id
|
||||
self._private_channels[channel_id] = channel
|
||||
|
||||
if self.is_bot and len(self._private_channels) > 128:
|
||||
if len(self._private_channels) > 128:
|
||||
_, to_remove = self._private_channels.popitem(last=False)
|
||||
if isinstance(to_remove, DMChannel):
|
||||
self._private_channels_by_user.pop(to_remove.recipient.id, None)
|
||||
@ -405,36 +401,34 @@ class ConnectionState:
|
||||
|
||||
async def _delay_ready(self):
|
||||
try:
|
||||
# only real bots wait for GUILD_CREATE streaming
|
||||
if self.is_bot:
|
||||
states = []
|
||||
while True:
|
||||
# this snippet of code is basically waiting N seconds
|
||||
# until the last GUILD_CREATE was sent
|
||||
try:
|
||||
guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
states = []
|
||||
while True:
|
||||
# this snippet of code is basically waiting N seconds
|
||||
# until the last GUILD_CREATE was sent
|
||||
try:
|
||||
guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
else:
|
||||
if self._guild_needs_chunking(guild):
|
||||
future = await self.chunk_guild(guild, wait=False)
|
||||
states.append((guild, future))
|
||||
else:
|
||||
if self._guild_needs_chunking(guild):
|
||||
future = await self.chunk_guild(guild, wait=False)
|
||||
states.append((guild, future))
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
for guild, future in states:
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
||||
for guild, future in states:
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
||||
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
# remove the state
|
||||
try:
|
||||
@ -442,10 +436,6 @@ class ConnectionState:
|
||||
except AttributeError:
|
||||
pass # already been deleted somehow
|
||||
|
||||
# call GUILD_SYNC after we're done chunking
|
||||
if not self.is_bot:
|
||||
log.info('Requesting GUILD_SYNC for %s guilds', len(self.guilds))
|
||||
await self.syncer([s.id for s in self.guilds])
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
else:
|
||||
@ -464,23 +454,19 @@ class ConnectionState:
|
||||
self.user = user = ClientUser(state=self, data=data['user'])
|
||||
self._users[user.id] = user
|
||||
|
||||
if self.application_id is None:
|
||||
try:
|
||||
application = data['application']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||
|
||||
for guild_data in data['guilds']:
|
||||
self._add_guild_from_data(guild_data)
|
||||
|
||||
for relationship in data.get('relationships', []):
|
||||
try:
|
||||
r_id = int(relationship['id'])
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
user._relationships[r_id] = Relationship(state=self, data=relationship)
|
||||
|
||||
for pm in data.get('private_channels', []):
|
||||
factory, _ = _channel_factory(pm['type'])
|
||||
self._add_private_channel(factory(me=user, data=pm, state=self))
|
||||
|
||||
self.dispatch('connect')
|
||||
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop)
|
||||
self._ready_task = asyncio.create_task(self._delay_ready())
|
||||
|
||||
def parse_resumed(self, data):
|
||||
self.dispatch('resumed')
|
||||
@ -601,6 +587,10 @@ class ConnectionState:
|
||||
if reaction:
|
||||
self.dispatch('reaction_clear_emoji', reaction)
|
||||
|
||||
def parse_interaction_create(self, data):
|
||||
interaction = Interaction(data=data, state=self)
|
||||
self.dispatch('interaction', interaction)
|
||||
|
||||
def parse_presence_update(self, data):
|
||||
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
guild = self._get_guild(guild_id)
|
||||
@ -724,22 +714,6 @@ class ConnectionState:
|
||||
else:
|
||||
self.dispatch('guild_channel_pins_update', channel, last_pin)
|
||||
|
||||
def parse_channel_recipient_add(self, data):
|
||||
channel = self._get_private_channel(int(data['channel_id']))
|
||||
user = self.store_user(data['user'])
|
||||
channel.recipients.append(user)
|
||||
self.dispatch('group_join', channel, user)
|
||||
|
||||
def parse_channel_recipient_remove(self, data):
|
||||
channel = self._get_private_channel(int(data['channel_id']))
|
||||
user = self.store_user(data['user'])
|
||||
try:
|
||||
channel.recipients.remove(user)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.dispatch('group_remove', channel, user)
|
||||
|
||||
def parse_guild_member_add(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
if guild is None:
|
||||
@ -868,7 +842,7 @@ class ConnectionState:
|
||||
|
||||
# check if it requires chunking
|
||||
if self._guild_needs_chunking(guild):
|
||||
asyncio.ensure_future(self._chunk_and_dispatch(guild, unavailable), loop=self.loop)
|
||||
asyncio.create_task(self._chunk_and_dispatch(guild, unavailable))
|
||||
return
|
||||
|
||||
# Dispatch available if newly available
|
||||
@ -877,10 +851,6 @@ class ConnectionState:
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
def parse_guild_sync(self, data):
|
||||
guild = self._get_guild(int(data['id']))
|
||||
guild._sync(data)
|
||||
|
||||
def parse_guild_update(self, data):
|
||||
guild = self._get_guild(int(data['id']))
|
||||
if guild is not None:
|
||||
@ -896,7 +866,7 @@ class ConnectionState:
|
||||
log.debug('GUILD_DELETE referencing an unknown guild ID: %s. Discarding.', data['id'])
|
||||
return
|
||||
|
||||
if data.get('unavailable', False) and guild is not None:
|
||||
if data.get('unavailable', False):
|
||||
# GUILD_DELETE with unavailable being True means that the
|
||||
# guild that was available is now currently unavailable
|
||||
guild.unavailable = True
|
||||
@ -928,10 +898,9 @@ class ConnectionState:
|
||||
|
||||
def parse_guild_ban_remove(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
if guild is not None:
|
||||
if 'user' in data:
|
||||
user = self.store_user(data['user'])
|
||||
self.dispatch('member_unban', guild, user)
|
||||
if guild is not None and 'user' in data:
|
||||
user = self.store_user(data['user'])
|
||||
self.dispatch('member_unban', guild, user)
|
||||
|
||||
def parse_guild_role_create(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
@ -1013,7 +982,7 @@ class ConnectionState:
|
||||
voice = self._get_voice_client(guild.id)
|
||||
if voice is not None:
|
||||
coro = voice.on_voice_state_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
||||
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
||||
|
||||
member, before, after = guild._update_voice_state(data, channel_id)
|
||||
if member is not None:
|
||||
@ -1027,11 +996,6 @@ class ConnectionState:
|
||||
self.dispatch('voice_state_update', member, before, after)
|
||||
else:
|
||||
log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id'])
|
||||
else:
|
||||
# in here we're either at private or group calls
|
||||
call = self._calls.get(channel_id)
|
||||
if call is not None:
|
||||
call._update_voice_state(data)
|
||||
|
||||
def parse_voice_server_update(self, data):
|
||||
try:
|
||||
@ -1042,7 +1006,7 @@ class ConnectionState:
|
||||
vc = self._get_voice_client(key_id)
|
||||
if vc is not None:
|
||||
coro = vc.on_voice_server_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||
|
||||
def parse_typing_start(self, data):
|
||||
channel, guild = self._get_guild_channel(data)
|
||||
@ -1053,32 +1017,19 @@ class ConnectionState:
|
||||
member = channel.recipient
|
||||
elif isinstance(channel, TextChannel) and guild is not None:
|
||||
member = guild.get_member(user_id)
|
||||
if member is None:
|
||||
member_data = data.get('member')
|
||||
if member_data:
|
||||
member = Member(data=member_data, state=self, guild=guild)
|
||||
|
||||
elif isinstance(channel, GroupChannel):
|
||||
member = utils.find(lambda x: x.id == user_id, channel.recipients)
|
||||
|
||||
if member is not None:
|
||||
timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp'))
|
||||
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
|
||||
self.dispatch('typing', channel, member, timestamp)
|
||||
|
||||
def parse_relationship_add(self, data):
|
||||
key = int(data['id'])
|
||||
old = self.user.get_relationship(key)
|
||||
new = Relationship(state=self, data=data)
|
||||
self.user._relationships[key] = new
|
||||
if old is not None:
|
||||
self.dispatch('relationship_update', old, new)
|
||||
else:
|
||||
self.dispatch('relationship_add', new)
|
||||
|
||||
def parse_relationship_remove(self, data):
|
||||
key = int(data['id'])
|
||||
try:
|
||||
old = self.user._relationships.pop(key)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.dispatch('relationship_remove', old)
|
||||
|
||||
def _get_reaction_user(self, channel, user_id):
|
||||
if isinstance(channel, TextChannel):
|
||||
return channel.guild.get_member(user_id)
|
||||
@ -1216,16 +1167,20 @@ class AutoShardedConnectionState(ConnectionState):
|
||||
self.user = user = ClientUser(state=self, data=data['user'])
|
||||
self._users[user.id] = user
|
||||
|
||||
if self.application_id is None:
|
||||
try:
|
||||
application = data['application']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||
|
||||
for guild_data in data['guilds']:
|
||||
self._add_guild_from_data(guild_data)
|
||||
|
||||
if self._messages:
|
||||
self._update_message_references()
|
||||
|
||||
for pm in data.get('private_channels', []):
|
||||
factory, _ = _channel_factory(pm['type'])
|
||||
self._add_private_channel(factory(me=user, data=pm, state=self))
|
||||
|
||||
self.dispatch('connect')
|
||||
self.dispatch('shard_connect', data['__shard_id__'])
|
||||
|
||||
@ -1238,7 +1193,7 @@ class AutoShardedConnectionState(ConnectionState):
|
||||
gc.collect()
|
||||
|
||||
if self._ready_task is None:
|
||||
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop)
|
||||
self._ready_task = asyncio.create_task(self._delay_ready())
|
||||
|
||||
def parse_resumed(self, data):
|
||||
self.dispatch('resumed')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -29,6 +27,10 @@ from .asset import Asset
|
||||
from .utils import snowflake_time
|
||||
from .enums import StickerType, try_enum
|
||||
|
||||
__all__ = (
|
||||
'Sticker',
|
||||
)
|
||||
|
||||
class Sticker(Hashable):
|
||||
"""Represents a sticker
|
||||
|
||||
@ -93,7 +95,7 @@ class Sticker(Hashable):
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC as a naive datetime."""
|
||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -43,10 +41,6 @@ class _PartialTemplateState:
|
||||
self.__state = state
|
||||
self.http = _FriendlyHttpAttributeErrorHelper()
|
||||
|
||||
@property
|
||||
def is_bot(self):
|
||||
return self.__state.is_bot
|
||||
|
||||
@property
|
||||
def shard_count(self):
|
||||
return self.__state.shard_count
|
||||
@ -76,7 +70,7 @@ class _PartialTemplateState:
|
||||
return []
|
||||
|
||||
def __getattr__(self, attr):
|
||||
raise AttributeError('PartialTemplateState does not support {0!r}.'.format(attr))
|
||||
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
|
||||
|
||||
class Template:
|
||||
"""Represents a Discord template.
|
||||
@ -96,16 +90,19 @@ class Template:
|
||||
creator: :class:`User`
|
||||
The creator of the template.
|
||||
created_at: :class:`datetime.datetime`
|
||||
When the template was created.
|
||||
An aware datetime in UTC representing when the template was created.
|
||||
updated_at: :class:`datetime.datetime`
|
||||
When the template was last updated (referred to as "last synced" in the client).
|
||||
An aware datetime in UTC representing when the template was last updated.
|
||||
This is referred to as "last synced" in the official Discord client.
|
||||
source_guild: :class:`Guild`
|
||||
The source guild.
|
||||
"""
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self._store(data)
|
||||
|
||||
def _store(self, data):
|
||||
self.code = data['code']
|
||||
self.uses = data['usage_count']
|
||||
self.name = data['name']
|
||||
@ -117,11 +114,16 @@ class Template:
|
||||
self.updated_at = parse_time(data.get('updated_at'))
|
||||
|
||||
id = _get_as_snowflake(data, 'source_guild_id')
|
||||
source_serialised = data['serialized_source_guild']
|
||||
source_serialised['id'] = id
|
||||
state = _PartialTemplateState(state=self._state)
|
||||
|
||||
self.source_guild = Guild(data=source_serialised, state=state)
|
||||
guild = self._state._get_guild(id)
|
||||
|
||||
if guild is None:
|
||||
source_serialised = data['serialized_source_guild']
|
||||
source_serialised['id'] = id
|
||||
state = _PartialTemplateState(state=self._state)
|
||||
guild = Guild(data=source_serialised, state=state)
|
||||
|
||||
self.source_guild = guild
|
||||
|
||||
def __repr__(self):
|
||||
return '<Template code={0.code!r} uses={0.uses} name={0.name!r}' \
|
||||
@ -147,9 +149,9 @@ class Template:
|
||||
|
||||
Raises
|
||||
------
|
||||
:exc:`.HTTPException`
|
||||
HTTPException
|
||||
Guild creation failed.
|
||||
:exc:`.InvalidArgument`
|
||||
InvalidArgument
|
||||
Invalid icon image format given. Must be PNG or JPG.
|
||||
|
||||
Returns
|
||||
@ -161,10 +163,81 @@ class Template:
|
||||
if icon is not None:
|
||||
icon = _bytes_to_base64_data(icon)
|
||||
|
||||
if region is None:
|
||||
region = VoiceRegion.us_west.value
|
||||
else:
|
||||
region = region.value
|
||||
region = region or VoiceRegion.us_west
|
||||
region_value = region.value
|
||||
|
||||
data = await self._state.http.create_from_template(self.code, name, region, icon)
|
||||
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
|
||||
return Guild(data=data, state=self._state)
|
||||
|
||||
async def sync(self):
|
||||
"""|coro|
|
||||
|
||||
Sync the template to the guild's current state.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
|
||||
data = await self._state.http.sync_template(self.source_guild.id, self.code)
|
||||
self._store(data)
|
||||
|
||||
async def edit(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Edit the template metadata.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: Optional[:class:`str`]
|
||||
The template's new name.
|
||||
description: Optional[:class:`str`]
|
||||
The template's description.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
|
||||
self._store(data)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Delete the template.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
await self._state.http.delete_template(self.source_guild.id, self.code)
|
||||
|
10
discord/types/__init__.py
Normal file
10
discord/types/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
discord.types
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Typings for the Discord API
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
91
discord/types/channel.py
Normal file
91
discord/types/channel.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
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 .user import PartialUser
|
||||
from .snowflake import Snowflake
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
|
||||
class PermissionOverwrite(TypedDict):
|
||||
id: Snowflake
|
||||
type: Literal[0, 1]
|
||||
allow: str
|
||||
deny: str
|
||||
|
||||
|
||||
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
|
||||
|
||||
|
||||
class PartialChannel(TypedDict):
|
||||
id: str
|
||||
type: ChannelType
|
||||
name: str
|
||||
|
||||
|
||||
class _TextChannelOptional(PartialChannel, total=False):
|
||||
topic: str
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: int
|
||||
rate_limit_per_user: int
|
||||
|
||||
|
||||
class _VoiceChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
|
||||
|
||||
class _CategoryChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StoreChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StageChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
topic: str
|
||||
|
||||
|
||||
class GuildChannel(
|
||||
_TextChannelOptional, _VoiceChannelOptional, _CategoryChannelOptional, _StoreChannelOptional, _StageChannelOptional
|
||||
):
|
||||
guild_id: Snowflake
|
||||
position: int
|
||||
permission_overwrites: List[PermissionOverwrite]
|
||||
nsfw: bool
|
||||
parent_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class DMChannel(PartialChannel):
|
||||
last_message_id: Optional[Snowflake]
|
||||
recipients: List[PartialUser]
|
||||
|
||||
|
||||
class GroupDMChannel(DMChannel):
|
||||
icon: Optional[str]
|
||||
owner_id: Snowflake
|
84
discord/types/embed.py
Normal file
84
discord/types/embed.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
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 typing import List, Literal, TypedDict
|
||||
|
||||
class _EmbedFooterOptional(TypedDict, total=False):
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
class EmbedFooter(_EmbedFooterOptional):
|
||||
text: str
|
||||
|
||||
class _EmbedFieldOptional(TypedDict, total=False):
|
||||
inline: bool
|
||||
|
||||
class EmbedField(_EmbedFieldOptional):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
class EmbedThumbnail(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedVideo(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedImage(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedProvider(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
class EmbedAuthor(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
|
||||
|
||||
class Embed(TypedDict, total=False):
|
||||
title: str
|
||||
type: EmbedType
|
||||
description: str
|
||||
url: str
|
||||
timestamp: str
|
||||
color: int
|
||||
footer: EmbedFooter
|
||||
image: EmbedImage
|
||||
thumbnail: EmbedThumbnail
|
||||
video: EmbedVideo
|
||||
provider: EmbedProvider
|
||||
author: EmbedAuthor
|
||||
fields: List[EmbedField]
|
28
discord/types/snowflake.py
Normal file
28
discord/types/snowflake.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""
|
||||
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 typing import List
|
||||
|
||||
Snowflake = str
|
||||
SnowflakeList = List[Snowflake]
|
33
discord/types/user.py
Normal file
33
discord/types/user.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""
|
||||
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 .snowflake import Snowflake
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
|
||||
class PartialUser(TypedDict):
|
||||
id: Snowflake
|
||||
username: str
|
||||
discriminator: str
|
||||
avatar: Optional[str]
|
541
discord/user.py
541
discord/user.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -24,61 +22,17 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import discord.abc
|
||||
from .flags import PublicUserFlags
|
||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time
|
||||
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse, PremiumType, try_enum
|
||||
from .errors import ClientException
|
||||
from .utils import snowflake_time, _bytes_to_base64_data
|
||||
from .enums import DefaultAvatar, try_enum
|
||||
from .colour import Colour
|
||||
from .asset import Asset
|
||||
|
||||
class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')):
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
def nitro(self):
|
||||
return self.premium_since is not None
|
||||
|
||||
premium = nitro
|
||||
|
||||
def _has_flag(self, o):
|
||||
v = o.value
|
||||
return (self.flags & v) == v
|
||||
|
||||
@property
|
||||
def staff(self):
|
||||
return self._has_flag(UserFlags.staff)
|
||||
|
||||
@property
|
||||
def partner(self):
|
||||
return self._has_flag(UserFlags.partner)
|
||||
|
||||
@property
|
||||
def bug_hunter(self):
|
||||
return self._has_flag(UserFlags.bug_hunter)
|
||||
|
||||
@property
|
||||
def early_supporter(self):
|
||||
return self._has_flag(UserFlags.early_supporter)
|
||||
|
||||
@property
|
||||
def hypesquad(self):
|
||||
return self._has_flag(UserFlags.hypesquad)
|
||||
|
||||
@property
|
||||
def hypesquad_houses(self):
|
||||
flags = (UserFlags.hypesquad_bravery, UserFlags.hypesquad_brilliance, UserFlags.hypesquad_balance)
|
||||
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
|
||||
|
||||
@property
|
||||
def team_user(self):
|
||||
return self._has_flag(UserFlags.team_user)
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
return self._has_flag(UserFlags.system)
|
||||
__all__ = (
|
||||
'User',
|
||||
'ClientUser',
|
||||
)
|
||||
|
||||
_BaseUser = discord.abc.User
|
||||
|
||||
@ -123,6 +77,7 @@ class BaseUser(_BaseUser):
|
||||
self.avatar = user.avatar
|
||||
self.bot = user.bot
|
||||
self._state = user._state
|
||||
self._public_flags = user._public_flags
|
||||
|
||||
return self
|
||||
|
||||
@ -197,7 +152,7 @@ class BaseUser(_BaseUser):
|
||||
@property
|
||||
def default_avatar_url(self):
|
||||
""":class:`Asset`: Returns a URL for a user's default avatar."""
|
||||
return Asset(self._state, '/embed/avatars/{}.png'.format(self.default_avatar.value))
|
||||
return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
@ -220,7 +175,7 @@ class BaseUser(_BaseUser):
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||
return '<@{0.id}>'.format(self)
|
||||
return f'<@{self.id}>'
|
||||
|
||||
def permissions_in(self, channel):
|
||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
||||
@ -242,7 +197,8 @@ class BaseUser(_BaseUser):
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the user's creation time in UTC.
|
||||
|
||||
This is when the user's Discord account was created."""
|
||||
This is when the user's Discord account was created.
|
||||
"""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
@ -272,11 +228,7 @@ class BaseUser(_BaseUser):
|
||||
if message.mention_everyone:
|
||||
return True
|
||||
|
||||
for user in message.mentions:
|
||||
if user.id == self.id:
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(user.id == self.id for user in message.mentions)
|
||||
|
||||
class ClientUser(BaseUser):
|
||||
"""Represents your Discord user.
|
||||
@ -318,24 +270,16 @@ class ClientUser(BaseUser):
|
||||
|
||||
verified: :class:`bool`
|
||||
Specifies if the user is a verified account.
|
||||
email: Optional[:class:`str`]
|
||||
The email the user used when registering.
|
||||
locale: Optional[:class:`str`]
|
||||
The IETF language tag used to identify the language the user is using.
|
||||
mfa_enabled: :class:`bool`
|
||||
Specifies if the user has MFA turned on and working.
|
||||
premium: :class:`bool`
|
||||
Specifies if the user is a premium user (e.g. has Discord Nitro).
|
||||
premium_type: Optional[:class:`PremiumType`]
|
||||
Specifies the type of premium a user has (e.g. Nitro or Nitro Classic). Could be None if the user is not premium.
|
||||
"""
|
||||
__slots__ = BaseUser.__slots__ + \
|
||||
('email', 'locale', '_flags', 'verified', 'mfa_enabled',
|
||||
'premium', 'premium_type', '_relationships', '__weakref__')
|
||||
('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
super().__init__(state=state, data=data)
|
||||
self._relationships = {}
|
||||
|
||||
def __repr__(self):
|
||||
return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
|
||||
@ -345,70 +289,16 @@ class ClientUser(BaseUser):
|
||||
super()._update(data)
|
||||
# There's actually an Optional[str] phone field as well but I won't use it
|
||||
self.verified = data.get('verified', False)
|
||||
self.email = data.get('email')
|
||||
self.locale = data.get('locale')
|
||||
self._flags = data.get('flags', 0)
|
||||
self.mfa_enabled = data.get('mfa_enabled', False)
|
||||
self.premium = data.get('premium', False)
|
||||
self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
|
||||
|
||||
def get_relationship(self, user_id):
|
||||
"""Retrieves the :class:`Relationship` if applicable.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user_id: :class:`int`
|
||||
The user ID to check if we have a relationship with them.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Relationship`]
|
||||
The relationship if available or ``None``.
|
||||
"""
|
||||
return self._relationships.get(user_id)
|
||||
|
||||
@property
|
||||
def relationships(self):
|
||||
"""List[:class:`User`]: Returns all the relationships that the user has.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return list(self._relationships.values())
|
||||
|
||||
@property
|
||||
def friends(self):
|
||||
r"""List[:class:`User`]: Returns all the users that the user is friends with.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend]
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
r"""List[:class:`User`]: Returns all the users that the user has blocked.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked]
|
||||
|
||||
async def edit(self, **fields):
|
||||
async def edit(self, *, username=None, avatar=None):
|
||||
"""|coro|
|
||||
|
||||
Edits the current profile of the client.
|
||||
|
||||
If a bot account is used then a password field is optional,
|
||||
otherwise it is required.
|
||||
|
||||
.. note::
|
||||
|
||||
To upload an avatar, a :term:`py:bytes-like object` must be passed in that
|
||||
@ -420,19 +310,6 @@ class ClientUser(BaseUser):
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
password: :class:`str`
|
||||
The current password for the client's account.
|
||||
Only applicable to user accounts.
|
||||
new_password: :class:`str`
|
||||
The new password you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
email: :class:`str`
|
||||
The new email you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
house: Optional[:class:`HypeSquadHouse`]
|
||||
The hypesquad house you wish to change to.
|
||||
Could be ``None`` to leave the current house.
|
||||
Only applicable to user accounts.
|
||||
username: :class:`str`
|
||||
The new username you wish to change to.
|
||||
avatar: :class:`bytes`
|
||||
@ -445,212 +322,14 @@ class ClientUser(BaseUser):
|
||||
Editing your profile failed.
|
||||
InvalidArgument
|
||||
Wrong image format passed for ``avatar``.
|
||||
ClientException
|
||||
Password is required for non-bot accounts.
|
||||
House field was not a HypeSquadHouse.
|
||||
"""
|
||||
|
||||
try:
|
||||
avatar_bytes = fields['avatar']
|
||||
except KeyError:
|
||||
avatar = self.avatar
|
||||
else:
|
||||
if avatar_bytes is not None:
|
||||
avatar = _bytes_to_base64_data(avatar_bytes)
|
||||
else:
|
||||
avatar = None
|
||||
|
||||
not_bot_account = not self.bot
|
||||
password = fields.get('password')
|
||||
if not_bot_account and password is None:
|
||||
raise ClientException('Password is required for non-bot accounts.')
|
||||
|
||||
args = {
|
||||
'password': password,
|
||||
'username': fields.get('username', self.name),
|
||||
'avatar': avatar
|
||||
}
|
||||
|
||||
if not_bot_account:
|
||||
args['email'] = fields.get('email', self.email)
|
||||
|
||||
if 'new_password' in fields:
|
||||
args['new_password'] = fields['new_password']
|
||||
|
||||
http = self._state.http
|
||||
|
||||
if 'house' in fields:
|
||||
house = fields['house']
|
||||
if house is None:
|
||||
await http.leave_hypesquad_house()
|
||||
elif not isinstance(house, HypeSquadHouse):
|
||||
raise ClientException('`house` parameter was not a HypeSquadHouse')
|
||||
else:
|
||||
value = house.value
|
||||
|
||||
await http.change_hypesquad_house(value)
|
||||
|
||||
data = await http.edit_profile(**args)
|
||||
if not_bot_account:
|
||||
self.email = data['email']
|
||||
try:
|
||||
http._token(data['token'], bot=False)
|
||||
except KeyError:
|
||||
pass
|
||||
if avatar is not None:
|
||||
avatar = _bytes_to_base64_data(avatar)
|
||||
|
||||
data = await self._state.http.edit_profile(username=username, avatar=avatar)
|
||||
self._update(data)
|
||||
|
||||
async def create_group(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Creates a group direct message with the recipients
|
||||
provided. These recipients must be have a relationship
|
||||
of type :attr:`RelationshipType.friend`.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument :class:`list` of :class:`User` to have in
|
||||
your group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Failed to create the group direct message.
|
||||
ClientException
|
||||
Attempted to create a group with only one recipient.
|
||||
This does not include yourself.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`GroupChannel`
|
||||
The new group channel.
|
||||
"""
|
||||
|
||||
from .channel import GroupChannel
|
||||
|
||||
if len(recipients) < 2:
|
||||
raise ClientException('You must have two or more recipients to create a group.')
|
||||
|
||||
users = [str(u.id) for u in recipients]
|
||||
data = await self._state.http.start_group(self.id, users)
|
||||
return GroupChannel(me=self, data=data, state=self._state)
|
||||
|
||||
async def edit_settings(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Edits the client user's settings.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-------
|
||||
afk_timeout: :class:`int`
|
||||
How long (in seconds) the user needs to be AFK until Discord
|
||||
sends push notifications to your mobile device.
|
||||
animate_emojis: :class:`bool`
|
||||
Whether or not to animate emojis in the chat.
|
||||
convert_emoticons: :class:`bool`
|
||||
Whether or not to automatically convert emoticons into emojis.
|
||||
e.g. :-) -> 😃
|
||||
default_guilds_restricted: :class:`bool`
|
||||
Whether or not to automatically disable DMs between you and
|
||||
members of new guilds you join.
|
||||
detect_platform_accounts: :class:`bool`
|
||||
Whether or not to automatically detect accounts from services
|
||||
like Steam and Blizzard when you open the Discord client.
|
||||
developer_mode: :class:`bool`
|
||||
Whether or not to enable developer mode.
|
||||
disable_games_tab: :class:`bool`
|
||||
Whether or not to disable the showing of the Games tab.
|
||||
enable_tts_command: :class:`bool`
|
||||
Whether or not to allow tts messages to be played/sent.
|
||||
explicit_content_filter: :class:`UserContentFilter`
|
||||
The filter for explicit content in all messages.
|
||||
friend_source_flags: :class:`FriendFlags`
|
||||
Who can add you as a friend.
|
||||
gif_auto_play: :class:`bool`
|
||||
Whether or not to automatically play gifs that are in the chat.
|
||||
guild_positions: List[:class:`abc.Snowflake`]
|
||||
A list of guilds in order of the guild/guild icons that are on
|
||||
the left hand side of the UI.
|
||||
inline_attachment_media: :class:`bool`
|
||||
Whether or not to display attachments when they are uploaded in chat.
|
||||
inline_embed_media: :class:`bool`
|
||||
Whether or not to display videos and images from links posted in chat.
|
||||
locale: :class:`str`
|
||||
The :rfc:`3066` language identifier of the locale to use for the language
|
||||
of the Discord client.
|
||||
message_display_compact: :class:`bool`
|
||||
Whether or not to use the compact Discord display mode.
|
||||
render_embeds: :class:`bool`
|
||||
Whether or not to render embeds that are sent in the chat.
|
||||
render_reactions: :class:`bool`
|
||||
Whether or not to render reactions that are added to messages.
|
||||
restricted_guilds: List[:class:`abc.Snowflake`]
|
||||
A list of guilds that you will not receive DMs from.
|
||||
show_current_game: :class:`bool`
|
||||
Whether or not to display the game that you are currently playing.
|
||||
status: :class:`Status`
|
||||
The clients status that is shown to others.
|
||||
theme: :class:`Theme`
|
||||
The theme of the Discord UI.
|
||||
timezone_offset: :class:`int`
|
||||
The timezone offset to use.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the settings failed.
|
||||
Forbidden
|
||||
The client is a bot user and not a user account.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`dict`
|
||||
The client user's updated settings.
|
||||
"""
|
||||
payload = {}
|
||||
|
||||
content_filter = kwargs.pop('explicit_content_filter', None)
|
||||
if content_filter:
|
||||
payload.update({'explicit_content_filter': content_filter.value})
|
||||
|
||||
friend_flags = kwargs.pop('friend_source_flags', None)
|
||||
if friend_flags:
|
||||
dicts = [{}, {'mutual_guilds': True}, {'mutual_friends': True},
|
||||
{'mutual_guilds': True, 'mutual_friends': True}, {'all': True}]
|
||||
payload.update({'friend_source_flags': dicts[friend_flags.value]})
|
||||
|
||||
guild_positions = kwargs.pop('guild_positions', None)
|
||||
if guild_positions:
|
||||
guild_positions = [str(x.id) for x in guild_positions]
|
||||
payload.update({'guild_positions': guild_positions})
|
||||
|
||||
restricted_guilds = kwargs.pop('restricted_guilds', None)
|
||||
if restricted_guilds:
|
||||
restricted_guilds = [str(x.id) for x in restricted_guilds]
|
||||
payload.update({'restricted_guilds': restricted_guilds})
|
||||
|
||||
status = kwargs.pop('status', None)
|
||||
if status:
|
||||
payload.update({'status': status.value})
|
||||
|
||||
theme = kwargs.pop('theme', None)
|
||||
if theme:
|
||||
payload.update({'theme': theme.value})
|
||||
|
||||
payload.update(kwargs)
|
||||
|
||||
data = await self._state.http.edit_settings(**payload)
|
||||
return data
|
||||
|
||||
class User(BaseUser, discord.abc.Messageable):
|
||||
"""Represents a Discord user.
|
||||
|
||||
@ -706,8 +385,22 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
"""
|
||||
return self._state._get_private_channel_by_user(self.id)
|
||||
|
||||
@property
|
||||
def mutual_guilds(self):
|
||||
"""List[:class:`Guild`]: The guilds that the user shares with the client.
|
||||
|
||||
.. note::
|
||||
|
||||
This will only return mutual guilds within the client's internal cache.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return [guild for guild in self._state._guilds.values() if guild.get_member(self.id)]
|
||||
|
||||
async def create_dm(self):
|
||||
"""Creates a :class:`DMChannel` with this user.
|
||||
"""|coro|
|
||||
|
||||
Creates a :class:`DMChannel` with this user.
|
||||
|
||||
This should be rarely called, as this is done transparently for most
|
||||
people.
|
||||
@ -724,171 +417,3 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
state = self._state
|
||||
data = await state.http.start_private_message(self.id)
|
||||
return state.add_dm_channel(data)
|
||||
|
||||
@property
|
||||
def relationship(self):
|
||||
"""Optional[:class:`Relationship`]: Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return self._state.user.get_relationship(self.id)
|
||||
|
||||
async def mutual_friends(self):
|
||||
"""|coro|
|
||||
|
||||
Gets all mutual friends of this user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to get mutual friends of this user.
|
||||
HTTPException
|
||||
Getting mutual friends failed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[:class:`User`]
|
||||
The users that are mutual friends.
|
||||
"""
|
||||
state = self._state
|
||||
mutuals = await state.http.get_mutual_friends(self.id)
|
||||
return [User(state=state, data=friend) for friend in mutuals]
|
||||
|
||||
def is_friend(self):
|
||||
""":class:`bool`: Checks if the user is your friend.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.friend
|
||||
|
||||
def is_blocked(self):
|
||||
""":class:`bool`: Checks if the user is blocked.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.blocked
|
||||
|
||||
async def block(self):
|
||||
"""|coro|
|
||||
|
||||
Blocks the user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to block this user.
|
||||
HTTPException
|
||||
Blocking the user failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
|
||||
|
||||
async def unblock(self):
|
||||
"""|coro|
|
||||
|
||||
Unblocks the user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to unblock this user.
|
||||
HTTPException
|
||||
Unblocking the user failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def remove_friend(self):
|
||||
"""|coro|
|
||||
|
||||
Removes the user as a friend.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to remove this user as a friend.
|
||||
HTTPException
|
||||
Removing the user as a friend failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def send_friend_request(self):
|
||||
"""|coro|
|
||||
|
||||
Sends the user a friend request.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to send a friend request to the user.
|
||||
HTTPException
|
||||
Sending the friend request failed.
|
||||
"""
|
||||
await self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator)
|
||||
|
||||
async def profile(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the user's profile.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to fetch profiles.
|
||||
HTTPException
|
||||
Fetching the profile failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Profile`
|
||||
The profile of the user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
data = await state.http.get_user_profile(self.id)
|
||||
|
||||
def transform(d):
|
||||
return state._get_guild(int(d['id']))
|
||||
|
||||
since = data.get('premium_since')
|
||||
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
|
||||
return Profile(flags=data['user'].get('flags', 0),
|
||||
premium_since=parse_time(since),
|
||||
mutual_guilds=mutual_guilds,
|
||||
user=self,
|
||||
connected_accounts=data['connected_accounts'])
|
||||
|
152
discord/utils.py
152
discord/utils.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -27,12 +25,13 @@ DEALINGS IN THE SOFTWARE.
|
||||
import array
|
||||
import asyncio
|
||||
import collections.abc
|
||||
from typing import Optional, overload
|
||||
import unicodedata
|
||||
from base64 import b64encode
|
||||
from bisect import bisect_left
|
||||
import datetime
|
||||
import functools
|
||||
from inspect import isawaitable as _isawaitable
|
||||
from inspect import isawaitable as _isawaitable, signature as _signature
|
||||
from operator import attrgetter
|
||||
import json
|
||||
import re
|
||||
@ -40,8 +39,19 @@ import warnings
|
||||
|
||||
from .errors import InvalidArgument
|
||||
|
||||
__all__ = (
|
||||
'oauth_uri',
|
||||
'snowflake_time',
|
||||
'time_snowflake',
|
||||
'find',
|
||||
'get',
|
||||
'sleep_until',
|
||||
'utcnow',
|
||||
'remove_markdown',
|
||||
'escape_markdown',
|
||||
'escape_mentions',
|
||||
)
|
||||
DISCORD_EPOCH = 1420070400000
|
||||
MAX_ASYNCIO_SECONDS = 3456000
|
||||
|
||||
class cached_property:
|
||||
def __init__(self, function):
|
||||
@ -105,11 +115,26 @@ class SequenceProxy(collections.abc.Sequence):
|
||||
def count(self, value):
|
||||
return self.__proxied.count(value)
|
||||
|
||||
def parse_time(timestamp):
|
||||
@overload
|
||||
def parse_time(timestamp: None) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_time(timestamp: str) -> datetime.datetime:
|
||||
...
|
||||
|
||||
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
|
||||
if timestamp:
|
||||
return datetime.datetime(*map(int, re.split(r'[^\d]', timestamp.replace('+00:00', ''))))
|
||||
return datetime.datetime.fromisoformat(timestamp)
|
||||
return None
|
||||
|
||||
def copy_doc(original):
|
||||
def decorator(overriden):
|
||||
overriden.__doc__ = original.__doc__
|
||||
overriden.__signature__ = _signature(original)
|
||||
return overriden
|
||||
return decorator
|
||||
|
||||
def deprecated(instead=None):
|
||||
def actual_decorator(func):
|
||||
@functools.wraps(func)
|
||||
@ -126,7 +151,7 @@ def deprecated(instead=None):
|
||||
return decorated
|
||||
return actual_decorator
|
||||
|
||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes=None):
|
||||
"""A helper function that returns the OAuth2 URL for inviting the bot
|
||||
into guilds.
|
||||
|
||||
@ -141,13 +166,18 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
The guild to pre-select in the authorization screen, if available.
|
||||
redirect_uri: :class:`str`
|
||||
An optional valid redirect URI.
|
||||
scopes: Iterable[:class:`str`]
|
||||
An optional valid list of scopes. Defaults to ``('bot',)``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The OAuth2 URL for inviting the bot into guilds.
|
||||
"""
|
||||
url = 'https://discord.com/oauth2/authorize?client_id={}&scope=bot'.format(client_id)
|
||||
url = f'https://discord.com/oauth2/authorize?client_id={client_id}'
|
||||
url = url + '&scope=' + '+'.join(scopes or ('bot',))
|
||||
if permissions is not None:
|
||||
url = url + '&permissions=' + str(permissions.value)
|
||||
if guild is not None:
|
||||
@ -158,7 +188,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
return url
|
||||
|
||||
|
||||
def snowflake_time(id):
|
||||
def snowflake_time(id: int) -> datetime.datetime:
|
||||
"""
|
||||
Parameters
|
||||
-----------
|
||||
@ -168,25 +198,34 @@ def snowflake_time(id):
|
||||
Returns
|
||||
--------
|
||||
:class:`datetime.datetime`
|
||||
The creation date in UTC of a Discord snowflake ID."""
|
||||
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000)
|
||||
An aware datetime in UTC representing the creation time of the snowflake.
|
||||
"""
|
||||
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
def time_snowflake(datetime_obj, high=False):
|
||||
def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
|
||||
"""Returns a numeric snowflake pretending to be created at the given date.
|
||||
|
||||
When using as the lower end of a range, use ``time_snowflake(high=False) - 1`` to be inclusive, ``high=True`` to be exclusive
|
||||
When using as the higher end of a range, use ``time_snowflake(high=True)`` + 1 to be inclusive, ``high=False`` to be exclusive
|
||||
When using as the lower end of a range, use ``time_snowflake(high=False) - 1``
|
||||
to be inclusive, ``high=True`` to be exclusive.
|
||||
|
||||
When using as the higher end of a range, use ``time_snowflake(high=True) + 1``
|
||||
to be inclusive, ``high=False`` to be exclusive
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
datetime_obj: :class:`datetime.datetime`
|
||||
A timezone-naive datetime object representing UTC time.
|
||||
dt: :class:`datetime.datetime`
|
||||
A datetime object to convert to a snowflake.
|
||||
If naive, the timezone is assumed to be local time.
|
||||
high: :class:`bool`
|
||||
Whether or not to set the lower 22 bit to high or low.
|
||||
"""
|
||||
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
|
||||
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The snowflake representing the time given.
|
||||
"""
|
||||
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
|
||||
return (discord_millis << 22) + (2**22-1 if high else 0)
|
||||
|
||||
def find(predicate, seq):
|
||||
@ -364,19 +403,31 @@ async def sleep_until(when, result=None):
|
||||
-----------
|
||||
when: :class:`datetime.datetime`
|
||||
The timestamp in which to sleep until. If the datetime is naive then
|
||||
it is assumed to be in UTC.
|
||||
it is assumed to be local time.
|
||||
result: Any
|
||||
If provided is returned to the caller when the coroutine completes.
|
||||
"""
|
||||
if when.tzinfo is None:
|
||||
when = when.replace(tzinfo=datetime.timezone.utc)
|
||||
when = when.astimezone()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
delta = (when - now).total_seconds()
|
||||
while delta > MAX_ASYNCIO_SECONDS:
|
||||
await asyncio.sleep(MAX_ASYNCIO_SECONDS)
|
||||
delta -= MAX_ASYNCIO_SECONDS
|
||||
return await asyncio.sleep(max(delta, 0), result)
|
||||
|
||||
def utcnow() -> datetime.datetime:
|
||||
"""A helper function to return an aware UTC datetime representing the current time.
|
||||
|
||||
This should be preferred to :func:`datetime.datetime.utcnow` since it is an aware
|
||||
datetime, compared to the naive datetime in the standard library.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`datetime.datetime`
|
||||
The current aware datetime in UTC.
|
||||
"""
|
||||
return datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
def valid_icon_size(size):
|
||||
"""Icons must be power of 2 within [16, 4096]."""
|
||||
return not size & (size - 1) and size in range(16, 4097)
|
||||
@ -419,11 +470,8 @@ def _string_width(string, *, _IS_ASCII=_IS_ASCII):
|
||||
return match.endpos
|
||||
|
||||
UNICODE_WIDE_CHAR_TYPE = 'WFA'
|
||||
width = 0
|
||||
func = unicodedata.east_asian_width
|
||||
for char in string:
|
||||
width += 2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1
|
||||
return width
|
||||
return sum(2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1 for char in string)
|
||||
|
||||
def resolve_invite(invite):
|
||||
"""
|
||||
@ -480,7 +528,44 @@ _MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(
|
||||
|
||||
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
|
||||
|
||||
_MARKDOWN_ESCAPE_REGEX = re.compile(r'(?P<markdown>%s|%s)' % (_MARKDOWN_ESCAPE_SUBREGEX, _MARKDOWN_ESCAPE_COMMON), re.MULTILINE)
|
||||
_MARKDOWN_ESCAPE_REGEX = re.compile(fr'(?P<markdown>{_MARKDOWN_ESCAPE_SUBREGEX}|{_MARKDOWN_ESCAPE_COMMON})', re.MULTILINE)
|
||||
|
||||
_URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
|
||||
|
||||
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
|
||||
|
||||
def remove_markdown(text, *, ignore_links=True):
|
||||
"""A helper function that removes markdown characters.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. note::
|
||||
This function is not markdown aware and may remove meaning from the original text. For example,
|
||||
if the input contains ``10 * 5`` then it will be converted into ``10 5``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
text: :class:`str`
|
||||
The text to remove markdown from.
|
||||
ignore_links: :class:`bool`
|
||||
Whether to leave links alone when removing markdown. For example,
|
||||
if a URL in the text contains characters such as ``_`` then it will
|
||||
be left alone. Defaults to ``True``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The text with the markdown special characters removed.
|
||||
"""
|
||||
|
||||
def replacement(match):
|
||||
groupdict = match.groupdict()
|
||||
return groupdict.get('url', '')
|
||||
|
||||
regex = _MARKDOWN_STOCK_REGEX
|
||||
if ignore_links:
|
||||
regex = f'(?:{_URL_REGEX}|{regex})'
|
||||
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
||||
|
||||
def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
r"""A helper function that escapes Discord's markdown.
|
||||
@ -508,7 +593,6 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
"""
|
||||
|
||||
if not as_needed:
|
||||
url_regex = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
|
||||
def replacement(match):
|
||||
groupdict = match.groupdict()
|
||||
is_url = groupdict.get('url')
|
||||
@ -516,9 +600,9 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
return is_url
|
||||
return '\\' + groupdict['markdown']
|
||||
|
||||
regex = r'(?P<markdown>[_\\~|\*`]|%s)' % _MARKDOWN_ESCAPE_COMMON
|
||||
regex = _MARKDOWN_STOCK_REGEX
|
||||
if ignore_links:
|
||||
regex = '(?:%s|%s)' % (url_regex, regex)
|
||||
regex = f'(?:{_URL_REGEX}|{regex})'
|
||||
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
||||
else:
|
||||
text = re.sub(r'\\', r'\\\\', text)
|
||||
@ -547,4 +631,4 @@ def escape_mentions(text):
|
||||
:class:`str`
|
||||
The text with the mentions removed.
|
||||
"""
|
||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,21})', '@\u200b\\1', text)
|
||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,20})', '@\u200b\\1', text)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -57,6 +55,11 @@ try:
|
||||
except ImportError:
|
||||
has_nacl = False
|
||||
|
||||
__all__ = (
|
||||
'VoiceProtocol',
|
||||
'VoiceClient',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class VoiceProtocol:
|
||||
@ -558,7 +561,7 @@ class VoiceClient(VoiceProtocol):
|
||||
raise ClientException('Already playing audio.')
|
||||
|
||||
if not isinstance(source, AudioSource):
|
||||
raise TypeError('source must an AudioSource not {0.__class__.__name__}'.format(source))
|
||||
raise TypeError(f'source must an AudioSource not {source.__class__.__name__}')
|
||||
|
||||
if not self.encoder and not source.is_opus():
|
||||
self.encoder = opus.Encoder()
|
||||
@ -601,7 +604,7 @@ class VoiceClient(VoiceProtocol):
|
||||
@source.setter
|
||||
def source(self, value):
|
||||
if not isinstance(value, AudioSource):
|
||||
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(value))
|
||||
raise TypeError(f'expected AudioSource not {value.__class__.__name__}.')
|
||||
|
||||
if self._player is None:
|
||||
raise ValueError('Not playing anything.')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -47,10 +45,104 @@ __all__ = (
|
||||
'RequestsWebhookAdapter',
|
||||
'Webhook',
|
||||
'WebhookMessage',
|
||||
'PartialWebhookChannel',
|
||||
'PartialWebhookGuild'
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PartialWebhookChannel(Hashable):
|
||||
"""Represents a partial channel for webhooks.
|
||||
|
||||
These are typically given for channel follower webhooks.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The partial channel's ID.
|
||||
name: :class:`str`
|
||||
The partial channel's name.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name')
|
||||
|
||||
def __init__(self, *, data):
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
|
||||
def __repr__(self):
|
||||
return f'<PartialWebhookChannel name={self.name!r} id={self.id}>'
|
||||
|
||||
class PartialWebhookGuild(Hashable):
|
||||
"""Represents a partial guild for webhooks.
|
||||
|
||||
These are typically given for channel follower webhooks.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The partial guild's ID.
|
||||
name: :class:`str`
|
||||
The partial guild's name.
|
||||
icon: :class:`str`
|
||||
The partial guild's icon
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name', 'icon', '_state')
|
||||
|
||||
def __init__(self, *, data, state):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.icon = data['icon']
|
||||
|
||||
def __repr__(self):
|
||||
return f'<PartialWebhookGuild name={self.name!r} id={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."""
|
||||
return bool(self.icon and self.icon.startswith('a_'))
|
||||
|
||||
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the guild's icon.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
|
||||
'gif' is only valid for animated avatars. The size must be a power of 2
|
||||
between 16 and 4096.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the icon to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected into either 'gif' or static_format depending on the
|
||||
icon being animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated icons to.
|
||||
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_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
|
||||
|
||||
class WebhookAdapter:
|
||||
"""Base class for all webhook adapters.
|
||||
|
||||
@ -65,7 +157,7 @@ class WebhookAdapter:
|
||||
def _prepare(self, webhook):
|
||||
self._webhook_id = webhook.id
|
||||
self._webhook_token = webhook.token
|
||||
self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token)
|
||||
self._request_url = f'{self.BASE}/webhooks/{webhook.id}/{webhook.token}'
|
||||
self.webhook = webhook
|
||||
|
||||
def is_async(self):
|
||||
@ -100,10 +192,10 @@ class WebhookAdapter:
|
||||
return self.request('PATCH', self._request_url, payload=payload, reason=reason)
|
||||
|
||||
def edit_webhook_message(self, message_id, payload):
|
||||
return self.request('PATCH', '{}/messages/{}'.format(self._request_url, message_id), payload=payload)
|
||||
return self.request('PATCH', f'{self._request_url}/messages/{message_id}', payload=payload)
|
||||
|
||||
def delete_webhook_message(self, message_id):
|
||||
return self.request('DELETE', '{}/messages/{}'.format(self._request_url, message_id))
|
||||
return self.request('DELETE', f'{self._request_url}/messages/{message_id}')
|
||||
|
||||
def handle_execution_response(self, data, *, wait):
|
||||
"""Transforms the webhook execution response into something
|
||||
@ -158,7 +250,7 @@ class WebhookAdapter:
|
||||
multipart = None
|
||||
files_to_pass = None
|
||||
|
||||
url = '%s?wait=%d' % (self._request_url, wait)
|
||||
url = f'{self._request_url}?wait={int(wait)}'
|
||||
maybe_coro = None
|
||||
try:
|
||||
maybe_coro = self.request('POST', url, multipart=multipart, payload=data, files=files_to_pass)
|
||||
@ -203,13 +295,6 @@ class AsyncWebhookAdapter(WebhookAdapter):
|
||||
if reason:
|
||||
headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ')
|
||||
|
||||
if multipart:
|
||||
data = aiohttp.FormData()
|
||||
for key, value in multipart.items():
|
||||
if key.startswith('file'):
|
||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
||||
else:
|
||||
data.add_field(key, value)
|
||||
|
||||
base_url = url.replace(self._request_url, '/') or '/'
|
||||
_id = self._webhook_id
|
||||
@ -217,6 +302,14 @@ class AsyncWebhookAdapter(WebhookAdapter):
|
||||
for file in files:
|
||||
file.reset(seek=tries)
|
||||
|
||||
if multipart:
|
||||
data = aiohttp.FormData()
|
||||
for key, value in multipart.items():
|
||||
if key.startswith('file'):
|
||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
||||
else:
|
||||
data.add_field(key, value)
|
||||
|
||||
async with self.session.request(verb, url, headers=headers, data=data) as r:
|
||||
log.debug('Webhook ID %s with %s %s has returned status code %s', _id, verb, base_url, r.status)
|
||||
# Coerce empty strings to return None for hygiene purposes
|
||||
@ -404,10 +497,6 @@ class _PartialWebhookState:
|
||||
def store_user(self, data):
|
||||
return BaseUser(state=self, data=data)
|
||||
|
||||
@property
|
||||
def is_bot(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
if self.parent is not None:
|
||||
@ -421,7 +510,7 @@ class _PartialWebhookState:
|
||||
if self.parent is not None:
|
||||
return getattr(self.parent, attr)
|
||||
|
||||
raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr))
|
||||
raise AttributeError(f'PartialWebhookState does not support {attr!r}.')
|
||||
|
||||
class WebhookMessage(Message):
|
||||
"""Represents a message sent from your webhook.
|
||||
@ -482,7 +571,7 @@ class WebhookMessage(Message):
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(inner_call(), loop=self._state.loop)
|
||||
asyncio.create_task(inner_call())
|
||||
return await asyncio.sleep(0)
|
||||
|
||||
def delete(self, *, delay=None):
|
||||
@ -596,10 +685,21 @@ class Webhook(Hashable):
|
||||
The default name of the webhook.
|
||||
avatar: Optional[:class:`str`]
|
||||
The default avatar of the webhook.
|
||||
source_guild: Optional[:class:`PartialWebhookGuild`]
|
||||
The guild of the channel that this webhook is following.
|
||||
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
source_channel: Optional[:class:`PartialWebhookChannel`]
|
||||
The channel that this webhook is following.
|
||||
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'type', 'guild_id', 'channel_id', 'user', 'name',
|
||||
'avatar', 'token', '_state', '_adapter')
|
||||
'avatar', 'token', '_state', '_adapter', 'source_channel', 'source_guild')
|
||||
|
||||
def __init__(self, data, *, adapter, state=None):
|
||||
self.id = int(data['id'])
|
||||
@ -621,13 +721,25 @@ class Webhook(Hashable):
|
||||
else:
|
||||
self.user = User(state=state, data=user)
|
||||
|
||||
source_channel = data.get('source_channel')
|
||||
if source_channel:
|
||||
source_channel = PartialWebhookChannel(data=source_channel)
|
||||
|
||||
self.source_channel = source_channel
|
||||
|
||||
source_guild = data.get('source_guild')
|
||||
if source_guild:
|
||||
source_guild = PartialWebhookGuild(data=source_guild, state=state)
|
||||
|
||||
self.source_guild = source_guild
|
||||
|
||||
def __repr__(self):
|
||||
return '<Webhook id=%r>' % self.id
|
||||
return f'<Webhook id={self.id!r}>'
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`str` : Returns the webhook's url."""
|
||||
return 'https://discord.com/api/webhooks/{}/{}'.format(self.id, self.token)
|
||||
return f'https://discord.com/api/webhooks/{self.id}/{self.token}'
|
||||
|
||||
@classmethod
|
||||
def partial(cls, id, token, *, adapter):
|
||||
@ -687,7 +799,7 @@ class Webhook(Hashable):
|
||||
A partial webhook is just a webhook object with an ID and a token.
|
||||
"""
|
||||
|
||||
m = re.search(r'discord(?:app)?.com/api/webhooks/(?P<id>[0-9]{17,21})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
|
||||
m = re.search(r'discord(?:app)?.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
|
||||
if m is None:
|
||||
raise InvalidArgument('Invalid webhook URL given.')
|
||||
data = m.groupdict()
|
||||
@ -696,7 +808,7 @@ class Webhook(Hashable):
|
||||
|
||||
@classmethod
|
||||
def _as_follower(cls, data, *, channel, user):
|
||||
name = "{} #{}".format(channel.guild, channel)
|
||||
name = f"{channel.guild} #{channel}"
|
||||
feed = {
|
||||
'id': data['webhook_id'],
|
||||
'type': 2,
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
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"),
|
||||
@ -30,6 +28,12 @@ from .activity import create_activity
|
||||
from .invite import Invite
|
||||
from .enums import Status, try_enum
|
||||
|
||||
__all__ = (
|
||||
'WidgetChannel',
|
||||
'WidgetMember',
|
||||
'Widget',
|
||||
)
|
||||
|
||||
class WidgetChannel:
|
||||
"""Represents a "partial" widget channel.
|
||||
|
||||
@ -77,7 +81,7 @@ class WidgetChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -156,7 +160,7 @@ class WidgetMember(BaseUser):
|
||||
@property
|
||||
def display_name(self):
|
||||
""":class:`str`: Returns the member's display name."""
|
||||
return self.nick if self.nick else self.name
|
||||
return self.nick or self.name
|
||||
|
||||
class Widget:
|
||||
"""Represents a :class:`Guild` widget.
|
||||
@ -236,7 +240,7 @@ class Widget:
|
||||
@property
|
||||
def json_url(self):
|
||||
""":class:`str`: The JSON URL of the widget."""
|
||||
return "https://discord.com/api/guilds/{0.id}/widget.json".format(self)
|
||||
return f"https://discord.com/api/guilds/{self.id}/widget.json"
|
||||
|
||||
@property
|
||||
def invite_url(self):
|
||||
|
Reference in New Issue
Block a user