fix conflicts

This commit is contained in:
iDutchy
2021-05-02 21:05:05 -05:00
84 changed files with 2683 additions and 706 deletions

View File

@@ -6,7 +6,7 @@ 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,8 +14,8 @@ 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__ = '1.7.1.7'
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -62,6 +62,6 @@ from .sticker import Sticker
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=1, minor=7, micro=1, enhanced=7, releaselevel='final', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -250,8 +250,9 @@ 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()
@@ -300,4 +301,5 @@ def main():
parser, args = parse_args()
args.func(parser, args)
main()
if __name__ == '__main__':
main()

View File

@@ -3,7 +3,7 @@
"""
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,6 +33,7 @@ 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
@@ -168,15 +169,15 @@ class _Overwrites:
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,
}
@@ -188,6 +189,7 @@ class GuildChannel:
- :class:`~discord.TextChannel`
- :class:`~discord.VoiceChannel`
- :class:`~discord.CategoryChannel`
- :class:`~discord.StageChannel`
This ABC must also implement :class:`~discord.abc.Snowflake`.
@@ -259,6 +261,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:
@@ -710,6 +719,127 @@ 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:`~discord.abc.Snowflake`
The channel that should be before our current channel.
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
after: :class:`~discord.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:`~discord.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|
@@ -904,7 +1034,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:
@@ -1146,9 +1276,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 +1284,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:

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -693,8 +693,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('Expected str, PartialEmoji, or None, received {0!r} instead.'.format(type(emoji)))
@property
def type(self):

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -61,30 +61,32 @@ class AppInfo:
A list of RPC origin URLs, if RPC is enabled.
summary: :class:`str`
If this application is a game sold on Discord,
this field will be the summary field for the store page of its primary SKU
this field will be the summary field for the store page of its primary SKU.
.. versionadded:: 1.3
verify_key: :class:`str`
The base64 encoded key for the GameSDK's GetTicket
The hex encoded key for verification in interactions and the
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
.. versionadded:: 1.3
guild_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the guild to which it has been linked
this field will be the guild to which it has been linked to.
.. versionadded:: 1.3
primary_sku_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the id of the "Game SKU" that is created, if exists
this field will be the id of the "Game SKU" that is created,
if it exists.
.. versionadded:: 1.3
slug: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the URL slug that links to the store page
this field will be the URL slug that links to the store page.
.. versionadded:: 1.3

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -165,7 +165,7 @@ class Asset:
format = 'gif' if emoji.animated else static_format
return cls(state, '/emojis/{0.id}.{1}'.format(emoji, format))
def __str__(self):
return self.BASE + self._url if self._url is not None else ''

View File

@@ -3,7 +3,7 @@
"""
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 +29,7 @@ from .object import Object
from .permissions import PermissionOverwrite, Permissions
from .colour import Colour
from .invite import Invite
from .mixins import Hashable
def _transform_verification_level(entry, data):
return enums.try_enum(enums.VerificationLevel, data)
@@ -51,8 +52,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:
@@ -187,11 +187,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`

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -36,6 +36,8 @@ class CallMessage:
This is only received in cases where the message type is equivalent to
:attr:`MessageType.call`.
.. deprecated:: 1.7
Attributes
-----------
ended_timestamp: Optional[:class:`datetime.datetime`]
@@ -53,12 +55,18 @@ class CallMessage:
@property
def call_ended(self):
""":class:`bool`: Indicates if the call has ended."""
""":class:`bool`: Indicates if the call has ended.
.. deprecated:: 1.7
"""
return self.ended_timestamp is not None
@property
def channel(self):
r""":class:`GroupChannel`\: The private channel associated with this message."""
r""":class:`GroupChannel`\: The private channel associated with this message.
.. deprecated:: 1.7
"""
return self.message.channel
@property
@@ -68,6 +76,8 @@ class CallMessage:
If the call has not ended then the current duration will
be returned.
.. deprecated:: 1.7
Returns
---------
:class:`datetime.timedelta`
@@ -83,6 +93,8 @@ class GroupCall:
This is accompanied with a :class:`CallMessage` denoting the information.
.. deprecated:: 1.7
Attributes
-----------
call: :class:`CallMessage`
@@ -122,7 +134,10 @@ class GroupCall:
@property
def connected(self):
"""List[:class:`User`]: A property that returns all users that are currently in this call."""
"""List[:class:`User`]: A property that returns all users that are currently in this call.
.. deprecated:: 1.7
"""
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
me = self.channel.me
if self.voice_state_for(me) is not None:
@@ -132,15 +147,21 @@ class GroupCall:
@property
def channel(self):
r""":class:`GroupChannel`\: Returns the channel the group call is in."""
r""":class:`GroupChannel`\: Returns the channel the group call is in.
.. deprecated:: 1.7
"""
return self.call.channel
@utils.deprecated()
def voice_state_for(self, user):
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
If the :class:`User` has no voice state then this function returns
``None``.
.. deprecated:: 1.7
Parameters
------------
user: :class:`User`

View File

@@ -3,7 +3,7 @@
"""
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 +29,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 +38,7 @@ from .errors import ClientException, NoMoreItems, InvalidArgument
__all__ = (
'TextChannel',
'VoiceChannel',
'StageChannel',
'DMChannel',
'CategoryChannel',
'StoreChannel',
@@ -148,6 +149,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 +158,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 +247,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 +255,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|
@@ -354,7 +353,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
fall back to single delete if current account is a user bot (now deprecated), or if messages are
older than two weeks.
Raises
@@ -546,7 +545,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,20 +656,20 @@ 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),
@@ -605,80 +677,18 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
]
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
@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 +721,135 @@ 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)
]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
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 +930,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 +973,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 +1009,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 +1045,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 +1122,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 +1131,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|
@@ -1255,8 +1422,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,6 +1448,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return base
@utils.deprecated()
async def add_recipients(self, *recipients):
r"""|coro|
@@ -1291,6 +1459,8 @@ class GroupChannel(discord.abc.Messageable, Hashable):
add a recipient to the group, you must have a relationship
with the user of type :attr:`RelationshipType.friend`.
.. deprecated:: 1.7
Parameters
-----------
\*recipients: :class:`User`
@@ -1308,11 +1478,14 @@ class GroupChannel(discord.abc.Messageable, Hashable):
for recipient in recipients:
await req(self.id, recipient.id)
@utils.deprecated()
async def remove_recipients(self, *recipients):
r"""|coro|
Removes recipients from this group.
.. deprecated:: 1.7
Parameters
-----------
\*recipients: :class:`User`
@@ -1330,11 +1503,14 @@ class GroupChannel(discord.abc.Messageable, Hashable):
for recipient in recipients:
await req(self.id, recipient.id)
@utils.deprecated()
async def edit(self, **fields):
"""|coro|
Edits the group.
.. deprecated:: 1.7
Parameters
-----------
name: Optional[:class:`str`]
@@ -1392,5 +1568,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

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -576,6 +576,8 @@ class Client:
Keyword argument that specifies if the account logging on is a bot
token or not.
.. deprecated:: 1.7
Raises
------
:exc:`.LoginFailure`
@@ -590,10 +592,13 @@ class Client:
await self.http.static_login(token.strip(), bot=bot)
self._connection.is_bot = bot
@utils.deprecated('Client.close')
async def logout(self):
"""|coro|
Logs out of Discord and closes all connections.
.. deprecated:: 1.7
.. note::
@@ -754,7 +759,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,16 +838,14 @@ 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))
@property
def intents(self):
""":class:`Intents`: The intents configured for this connection.
""":class:`~discord.Intents`: The intents configured for this connection.
.. versionadded:: 1.5
"""
@@ -1154,9 +1157,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::
@@ -1306,15 +1307,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
@@ -1480,7 +1479,7 @@ class Client:
.. note::
This method is an API call. For general usage, consider :meth:`get_user` instead.
This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_user` instead.
Parameters
-----------
@@ -1502,11 +1501,14 @@ class Client:
data = await self.http.get_user(user_id)
return User(state=self._connection, data=data)
@utils.deprecated()
async def fetch_user_profile(self, user_id):
"""|coro|
Gets an arbitrary user's profile.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -119,18 +119,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``."""

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -82,6 +82,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.
@@ -120,6 +122,15 @@ class Embed:
self.url = kwargs.get('url', EmptyEmbed)
self.description = kwargs.get('description', EmptyEmbed)
if self.title is not EmptyEmbed:
self.title = str(self.title)
if self.description is not EmptyEmbed:
self.description = str(self.description)
if self.url is not EmptyEmbed:
self.url = str(self.url)
try:
timestamp = kwargs['timestamp']
except KeyError:
@@ -153,6 +164,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:
@@ -422,7 +442,7 @@ class Embed:
@property
def fields(self):
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
See :meth:`add_field` for possible values you can access.

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -158,24 +158,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'

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -104,7 +104,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))

View File

@@ -6,7 +6,7 @@ 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.
"""

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -110,6 +110,7 @@ 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.')
@@ -190,9 +191,8 @@ 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)
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
@@ -656,7 +656,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 +678,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 +699,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 +709,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 +726,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 +750,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 +764,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 +777,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 +802,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 +954,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
@@ -1054,6 +1091,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

View File

@@ -3,7 +3,7 @@
"""
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 +101,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):
@@ -136,7 +136,7 @@ class CogMeta(type):
commands[elem] = value
elif inspect.iscoroutinefunction(value):
try:
is_listener = getattr(value, '__cog_listener__')
getattr(value, '__cog_listener__')
except AttributeError:
continue
else:
@@ -202,7 +202,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
@@ -306,6 +306,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 +418,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

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -58,6 +58,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 +88,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)
@@ -184,13 +193,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 +213,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 +226,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 +239,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 +323,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 +345,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__

View File

@@ -3,7 +3,7 @@
"""
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,17 +37,21 @@ __all__ = (
'MemberConverter',
'UserConverter',
'MessageConverter',
'PartialMessageConverter',
'TextChannelConverter',
'InviteConverter',
'GuildConverter',
'RoleConverter',
'GameConverter',
'ColourConverter',
'ColorConverter',
'VoiceChannelConverter',
'StageChannelConverter',
'EmojiConverter',
'PartialEmojiConverter',
'CategoryChannelConverter',
'IDConverter',
'StoreChannelConverter',
'clean_content',
'Greedy',
)
@@ -100,7 +104,7 @@ class Converter:
class IDConverter(Converter):
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):
@@ -251,7 +255,38 @@ class UserConverter(IDConverter):
return result
class MessageConverter(Converter):
class PartialMessageConverter(Converter):
"""Converts to a :class:`discord.PartialMessage`.
.. versionadded:: 1.7
The creation strategy is as follows (in order):
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
2. By message ID (The message is assumed to be in the context channel.)
3. By message URL
"""
def _get_id_matches(self, argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
link_regex = re.compile(
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
r'(?:[0-9]{15,20}|@me)'
r'/(?P<channel_id>[0-9]{15,20})/(?P<message_id>[0-9]{15,20})/?$'
)
match = id_regex.match(argument) or link_regex.match(argument)
if not match:
raise MessageNotFound(argument)
channel_id = match.group("channel_id")
return int(match.group("message_id")), int(channel_id) if channel_id else None
async def convert(self, ctx, argument):
message_id, channel_id = self._get_id_matches(argument)
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
if not channel:
raise ChannelNotFound(channel_id)
return discord.PartialMessage(channel=channel, id=message_id)
class MessageConverter(PartialMessageConverter):
"""Converts to a :class:`discord.Message`.
.. versionadded:: 1.1
@@ -266,21 +301,11 @@ class MessageConverter(Converter):
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")
message_id, channel_id = self._get_id_matches(argument)
message = ctx.bot._connection._get_message(message_id)
if message:
return message
channel = ctx.bot.get_channel(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:
@@ -373,6 +398,46 @@ class VoiceChannelConverter(IDConverter):
return result
class StageChannelConverter(IDConverter):
"""Converts to a :class:`~discord.StageChannel`.
.. versionadded:: 1.7
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.stage_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StageChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StageChannel):
raise ChannelNotFound(argument)
return result
class CategoryChannelConverter(IDConverter):
"""Converts to a :class:`~discord.CategoryChannel`.
@@ -415,6 +480,47 @@ class CategoryChannelConverter(IDConverter):
return result
class StoreChannelConverter(IDConverter):
"""Converts to a :class:`~discord.StoreChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name.
.. versionadded:: 1.7
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StoreChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StoreChannel):
raise ChannelNotFound(argument)
return result
class ColourConverter(Converter):
"""Converts to a :class:`~discord.Colour`.
@@ -426,37 +532,84 @@ 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, argument):
if argument[0] == '#':
return self.parse_hex_number(argument[1:])
if argument[0:2] == '0x':
rest = argument[2:]
# Legacy backwards compatible syntax
if rest.startswith('#'):
return self.parse_hex_number(rest[1:])
return self.parse_hex_number(rest)
arg = argument.lower()
if arg[0:3] == 'rgb':
return self.parse_rgb(arg)
arg = arg.replace(' ', '_')
method = getattr(discord.Colour, arg, None)
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
ColorConverter = ColourConverter
class RoleConverter(IDConverter):
"""Converts to a :class:`~discord.Role`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
All lookups are via the local guild. If in a DM context, the converter raises
:exc:`.NoPrivateMessage` exception.
The lookup strategy is as follows (in order):
@@ -502,6 +655,32 @@ class InviteConverter(Converter):
except Exception as exc:
raise BadInviteArgument() from exc
class GuildConverter(IDConverter):
"""Converts to a :class:`~discord.Guild`.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
.. versionadded:: 1.7
"""
async def convert(self, ctx, argument):
match = self._get_id_match(argument)
result = None
if match is not None:
guild_id = int(match.group(1))
result = ctx.bot.get_guild(guild_id)
if result is None:
result = discord.utils.get(ctx.bot.guilds, name=argument)
if result is None:
raise GuildNotFound(argument)
return result
class EmojiConverter(IDConverter):
"""Converts to a :class:`~discord.Emoji`.
@@ -580,11 +759,16 @@ 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):
message = ctx.message
@@ -635,6 +819,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)

View File

@@ -3,7 +3,7 @@
"""
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 +66,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 +81,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 +154,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

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -158,7 +158,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
@@ -523,7 +523,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 +693,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 +715,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
@@ -904,6 +903,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.
@@ -1335,6 +1341,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 +1381,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)
@@ -1949,8 +1959,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):

View File

@@ -3,7 +3,7 @@
"""
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 +45,7 @@ __all__ = (
'NotOwner',
'MessageNotFound',
'MemberNotFound',
'GuildNotFound',
'UserNotFound',
'ChannelNotFound',
'ChannelNotReadable',
@@ -230,6 +231,22 @@ class MemberNotFound(BadArgument):
self.argument = argument
super().__init__('Member "{}" not found.'.format(argument))
class GuildNotFound(BadArgument):
"""Exception raised when the guild provided was not found in the bot's cache.
This inherits from :exc:`BadArgument`
.. versionadded:: 1.7
Attributes
-----------
argument: :class:`str`
The guild supplied by the called that was not found
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Guild "{}" not found.'.format(argument))
class UserNotFound(BadArgument):
"""Exception raised when the user provided was not found in the bot's
cache.

View File

@@ -3,7 +3,7 @@
"""
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 +79,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 +108,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 +130,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))
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 +170,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 +272,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 +321,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 +336,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 +349,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 +366,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 +375,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
@@ -425,15 +424,24 @@ 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
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)
@@ -560,11 +568,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:

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -103,19 +103,19 @@ 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
await asyncio.sleep(backoff.delay())
else:
await sleep_until(self._next_iteration)
if self._stop_next_iteration:
return
self._current_loop += 1
if self._current_loop == self.count:
break
await sleep_until(self._next_iteration)
except asyncio.CancelledError:
self._is_being_cancelled = True
raise
@@ -154,14 +154,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.

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -58,7 +58,7 @@ 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
@@ -92,6 +92,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

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -492,7 +492,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>`.
@@ -954,4 +954,4 @@ class MemberCacheFlags(BaseFlags):
@property
def _online_only(self):
return self.value == 1
return self.value == 1

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -422,16 +422,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)
@@ -876,7 +871,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)

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -95,7 +95,7 @@ class Guild(Hashable):
The guild owner's ID. Use :attr:`Guild.owner` instead.
unavailable: :class:`bool`
Indicates if the guild is unavailable. If this is ``True`` then the
reliability of other attributes outside of :meth:`Guild.id` is slim and they might
reliability of other attributes outside of :attr:`Guild.id` is slim and they might
all be ``None``. It is best to not do anything with the guild if it is unavailable.
Check the :func:`on_guild_unavailable` and :func:`on_guild_available` events.
@@ -205,7 +205,7 @@ 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
@@ -374,6 +374,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 +873,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 +981,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 +1008,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|
@@ -1069,8 +1125,8 @@ class Guild(Hashable):
The new description of the guild. This is only available to guilds that
contain ``PUBLIC`` in :attr:`Guild.features`.
icon: :class:`bytes`
A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG supported
and GIF This is only available to guilds that contain ``ANIMATED_ICON`` in :attr:`Guild.features`.
A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG is supported.
GIF is only available to guilds that contain ``ANIMATED_ICON`` in :attr:`Guild.features`.
Could be ``None`` to denote removal of the icon.
banner: :class:`bytes`
A :term:`py:bytes-like object` representing the banner.
@@ -1270,7 +1326,7 @@ class Guild(Hashable):
def convert(d):
factory, ch_type = _channel_factory(d['type'])
if factory is None:
raise InvalidData('Unknown channel type {type} for channel ID {id}.'.format_map(data))
raise InvalidData('Unknown channel type {type} for channel ID {id}.'.format_map(d))
channel = factory(guild=self, state=self._state, data=d)
return channel
@@ -1278,9 +1334,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::
@@ -1370,11 +1424,11 @@ 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::
This method is an API call. For general usage, consider :meth:`get_member` instead.
This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_member` instead.
Parameters
-----------
@@ -1399,9 +1453,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 +1474,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 +1486,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 +1500,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)
@@ -1521,6 +1568,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 +1613,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 +1624,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
-------
@@ -1573,7 +1648,10 @@ class Guild(Hashable):
if not isinstance(days, int):
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
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 +1685,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 +1894,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`
@@ -2031,6 +2142,7 @@ class Guild(Hashable):
payload['max_age'] = 0
return Invite(state=self._state, data=payload)
@utils.deprecated()
def ack(self):
"""|coro|
@@ -2038,6 +2150,8 @@ class Guild(Hashable):
The user must not be a bot user.
.. deprecated:: 1.7
Raises
-------
HTTPException
@@ -2162,7 +2276,8 @@ class Guild(Hashable):
if not self._state._intents.members:
raise ClientException('Intents.members must be enabled to use this.')
return await self._state.chunk_guild(self, cache=cache)
if not self._state.is_guild_evicted(self):
return await self._state.chunk_guild(self, cache=cache)
async def query_members(self, query=None, *, limit=5, user_ids=None, presences=False, cache=True):
"""|coro|
@@ -2224,6 +2339,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)

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -131,7 +131,7 @@ class HTTPClient:
return await self.__session.ws_connect(url, **kwargs)
async def request(self, route, *, files=None, **kwargs):
async def request(self, route, *, files=None, form=None, **kwargs):
bucket = route.bucket
method = route.method
url = route.url
@@ -181,6 +181,13 @@ class HTTPClient:
if files:
for f in files:
f.reset(seek=tries)
if form:
form_data = aiohttp.FormData()
for params in form:
form_data.add_field(**params)
kwargs['data'] = form_data
try:
async with self.__session.request(method, url, **kwargs) as r:
log.debug('%s %s with %s has returned %s', method, url, kwargs.get('data'), r.status)
@@ -371,7 +378,7 @@ class HTTPClient:
def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, nonce=None, allowed_mentions=None, message_reference=None):
r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id)
form = aiohttp.FormData()
form = []
payload = {'tts': tts}
if content:
@@ -385,15 +392,25 @@ class HTTPClient:
if message_reference:
payload['message_reference'] = message_reference
form.add_field('payload_json', utils.to_json(payload))
form.append({'name': 'payload_json', 'value': utils.to_json(payload)})
if len(files) == 1:
file = files[0]
form.add_field('file', file.fp, filename=file.filename, content_type='application/octet-stream')
form.append({
'name': 'file',
'value': file.fp,
'filename': file.filename,
'content_type': 'application/octet-stream'
})
else:
for index, file in enumerate(files):
form.add_field('file%s' % index, file.fp, filename=file.filename, content_type='application/octet-stream')
form.append({
'name': 'file%s' % index,
'value': file.fp,
'filename': file.filename,
'content_type': 'application/octet-stream'
})
return self.request(r, data=form, files=files)
return self.request(r, form=form, files=files)
async def ack_message(self, channel_id, message_id):
r = Route('POST', '/channels/{channel_id}/messages/{message_id}/ack', channel_id=channel_id, message_id=message_id)
@@ -557,6 +574,14 @@ class HTTPClient:
}
return self.request(r, json=payload, reason=reason)
def edit_my_voice_state(self, guild_id, payload):
r = Route('PATCH', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)
return self.request(r, json=payload)
def edit_voice_state(self, guild_id, user_id, payload):
r = Route('PATCH', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)
return self.request(r, json=payload)
def edit_member(self, guild_id, user_id, *, reason=None, **fields):
r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id)
return self.request(r, json=fields, reason=reason)
@@ -567,11 +592,10 @@ class HTTPClient:
r = Route('PATCH', '/channels/{channel_id}', channel_id=channel_id)
valid_keys = ('name', 'parent_id', 'topic', 'bitrate', 'nsfw',
'user_limit', 'position', 'permission_overwrites', 'rate_limit_per_user',
'type')
'type', 'rtc_region')
payload = {
k: v for k, v in options.items() if k in valid_keys
}
return self.request(r, reason=reason, json=payload)
def bulk_channel_update(self, guild_id, data, *, reason=None):
@@ -584,7 +608,8 @@ class HTTPClient:
}
valid_keys = ('name', 'parent_id', 'topic', 'bitrate', 'nsfw',
'user_limit', 'position', 'permission_overwrites', 'rate_limit_per_user')
'user_limit', 'position', 'permission_overwrites', 'rate_limit_per_user',
'rtc_region')
payload.update({
k: v for k, v in options.items() if k in valid_keys and v is not None
})
@@ -670,6 +695,28 @@ class HTTPClient:
def get_template(self, code):
return self.request(Route('GET', '/guilds/templates/{code}', code=code))
def guild_templates(self, guild_id):
return self.request(Route('GET', '/guilds/{guild_id}/templates', guild_id=guild_id))
def create_template(self, guild_id, payload):
return self.request(Route('POST', '/guilds/{guild_id}/templates', guild_id=guild_id), json=payload)
def sync_template(self, guild_id, code):
return self.request(Route('PUT', '/guilds/{guild_id}/templates/{code}', guild_id=guild_id, code=code))
def edit_template(self, guild_id, code, payload):
valid_keys = (
'name',
'description',
)
payload = {
k: v for k, v in payload.items() if k in valid_keys
}
return self.request(Route('PATCH', '/guilds/{guild_id}/templates/{code}', guild_id=guild_id, code=code), json=payload)
def delete_template(self, guild_id, code):
return self.request(Route('DELETE', '/guilds/{guild_id}/templates/{code}', guild_id=guild_id, code=code))
def create_from_template(self, code, name, region, icon):
payload = {
'name': name,
@@ -717,10 +764,13 @@ class HTTPClient:
return self.request(Route('POST', '/guilds/{guild_id}/prune', guild_id=guild_id), json=payload, reason=reason)
def estimate_pruned_members(self, guild_id, days):
def estimate_pruned_members(self, guild_id, days, roles):
params = {
'days': days
}
if roles:
params['include_roles'] = ', '.join(roles)
return self.request(Route('GET', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params)
def get_all_custom_emojis(self, guild_id):

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -258,7 +258,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`]]
@@ -274,6 +275,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`]

View File

@@ -3,7 +3,7 @@
"""
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 +101,7 @@ class _ChunkedAsyncIterator(_AsyncIterator):
def __init__(self, iterator, max_size):
self.iterator = iterator
self.max_size = max_size
async def next(self):
ret = []
n = 0
@@ -291,13 +291,10 @@ 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
@@ -447,13 +444,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
@@ -547,13 +541,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
@@ -636,13 +627,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

View File

@@ -3,7 +3,7 @@
"""
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 +24,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 +33,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
@@ -58,15 +61,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`]
A 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 +100,20 @@ 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)
]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
@@ -106,14 +136,19 @@ def flatten_user(cls):
# 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
@@ -153,6 +188,13 @@ class Member(discord.abc.Messageable, _BaseUser):
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.
.. note::
Due to a Discord API limitation, a user's Spotify activity may not appear
if they are listening to a song with a title longer
than 128 characters. See :issue:`1738` for more information.
guild: :class:`Guild`
The guild that the member belongs to.
nick: Optional[:class:`str`]
@@ -293,12 +335,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
@@ -401,13 +443,19 @@ 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):
"""Union[:class:`BaseActivity`, :class:`Spotify`]: Returns the primary
activity the user is currently doing. Could be ``None`` if no activity is being done.
.. note::
Due to a Discord API limitation, this may be ``None`` if
the user is listening to a song on Spotify with a title longer
than 128 characters. See :issue:`1738` for more information.
.. note::
A user may have multiple activities, these can be accessed under :attr:`activities`.
@@ -434,11 +482,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 +604,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 +626,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 +635,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 +649,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 +680,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|

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -68,9 +68,30 @@ def convert_emoji_reaction(emoji):
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
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 +111,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 +128,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 +140,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 +311,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 +329,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):
@@ -309,12 +345,13 @@ class MessageReference:
self.message_id = utils._get_as_snowflake(data, 'message_id')
self.channel_id = int(data.pop('channel_id'))
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.fail_if_not_exists = data.get('fail_if_not_exists', True)
self._state = state
self.resolved = None
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 +360,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 +398,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 +415,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,7 +471,7 @@ 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`]
@@ -427,6 +480,9 @@ class Message(Hashable):
call: Optional[:class:`CallMessage`]
The call that the message refers to. This is only applicable to messages of type
:attr:`MessageType.call`.
.. deprecated:: 1.7
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
@@ -516,6 +572,7 @@ class Message(Hashable):
self.application = data.get('application')
self.activity = data.get('activity')
self.channel = channel
self.call = None
self._edited_timestamp = utils.parse_time(data['edited_timestamp'])
self.type = try_enum(MessageType, data['type'])
self.pinned = data['pinned']
@@ -798,9 +855,9 @@ 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 = {
@@ -938,6 +995,21 @@ class Message(Hashable):
if self.type is MessageType.channel_follow_add:
return '{0.author.name} has added {0.content} to this channel'.format(self)
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, silent=False):
"""|coro|
@@ -1272,6 +1344,7 @@ class Message(Hashable):
"""
await self._state.http.clear_reactions(self.channel.id, self.id)
@utils.deprecated()
async def ack(self):
"""|coro|
@@ -1279,6 +1352,8 @@ class Message(Hashable):
The user must not be a bot user.
.. deprecated:: 1.7
Raises
-------
HTTPException
@@ -1295,8 +1370,8 @@ class Message(Hashable):
async def reply(self, content=None, **kwargs):
"""|coro|
A shortcut method to :meth:`abc.Messageable.send` to reply to the
:class:`Message`.
A shortcut method to :meth:`.abc.Messageable.send` to reply to the
:class:`.Message`.
.. versionadded:: 1.6
@@ -1312,24 +1387,32 @@ class Message(Hashable):
Returns
---------
:class:`Message`
:class:`.Message`
The message that was sent.
"""
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 +1472,6 @@ class PartialMessage(Hashable):
_exported_names = (
'jump_url',
'delete',
'edit',
'publish',
'pin',
'unpin',
@@ -1454,3 +1536,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)

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -276,17 +276,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()
@@ -342,9 +339,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 +369,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.

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -149,10 +149,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)

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -140,8 +140,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 +150,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 +204,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 +459,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)

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -93,14 +93,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 +113,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.

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
"""
from .enums import RelationshipType, try_enum
from . import utils
class Relationship:
"""Represents a relationship in Discord.
@@ -32,6 +33,8 @@ class Relationship:
A relationship is like a friendship, a person who is blocked, etc.
Only non-bot accounts can have relationships.
.. deprecated:: 1.7
Attributes
-----------
user: :class:`User`
@@ -50,11 +53,14 @@ class Relationship:
def __repr__(self):
return '<Relationship user={0.user!r} type={0.type!r}>'.format(self)
@utils.deprecated()
async def delete(self):
"""|coro|
Deletes the relationship.
.. deprecated:: 1.7
Raises
------
HTTPException
@@ -63,12 +69,15 @@ class Relationship:
await self._state.http.remove_relationship(self.user.id)
@utils.deprecated()
async def accept(self):
"""|coro|
Accepts the relationship request. e.g. accepting a
friend request.
.. deprecated:: 1.7
Raises
-------
HTTPException

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -191,7 +191,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)
@@ -343,7 +343,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)

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -413,7 +413,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:

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -827,6 +827,9 @@ class ConnectionState:
return self._add_guild_from_data(data)
def is_guild_evicted(self, guild) -> bool:
return guild.id not in self._guilds
async def chunk_guild(self, guild, *, wait=True, cache=None):
cache = cache or self.member_cache_flags.joined
request = self._chunk_requests.get(guild.id)
@@ -896,7 +899,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 +931,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']))
@@ -1053,6 +1055,11 @@ 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)

View File

@@ -3,7 +3,7 @@
"""
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,7 +30,7 @@ from .utils import snowflake_time
from .enums import StickerType, try_enum
class Sticker(Hashable):
"""Represents a sticker
"""Represents a sticker.
.. versionadded:: 1.6
@@ -38,34 +38,34 @@ class Sticker(Hashable):
.. describe:: str(x)
Returns the name of the sticker
Returns the name of the sticker.
.. describe:: x == y
Checks if the sticker is equal to another sticker
Checks if the sticker is equal to another sticker.
.. describe:: x != y
Checks if the sticker is not equal to another sticker
Checks if the sticker is not equal to another sticker.
Attributes
----------
name: :class:`str`
The sticker's name
The sticker's name.
id: :class:`int`
The id of the sticker
The id of the sticker.
description: :class:`str`
The description of the sticker
The description of the sticker.
pack_id: :class:`int`
The id of the sticker's pack
The id of the sticker's pack.
format: :class:`StickerType`
The format for the sticker's image
The format for the sticker's image.
image: :class:`str`
The sticker's image
The sticker's image.
tags: List[:class:`str`]
A list of tags for the sticker
A list of tags for the sticker.
preview_image: Optional[:class:`str`]
The sticker's preview asset hash
The sticker's preview asset hash.
"""
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags', 'preview_image')
@@ -101,7 +101,7 @@ class Sticker(Hashable):
"""Returns an :class:`Asset` for the sticker's image.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``
This will return ``None`` if the format is ``StickerType.lottie``.
Returns
-------

View File

@@ -3,7 +3,7 @@
"""
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"),

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -105,7 +105,9 @@ class Template:
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 +119,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 +154,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 +168,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)

View File

@@ -3,7 +3,7 @@
"""
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,6 +33,7 @@ from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse, P
from .errors import ClientException
from .colour import Colour
from .asset import Asset
from .utils import deprecated
class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')):
__slots__ = ()
@@ -123,6 +124,7 @@ class BaseUser(_BaseUser):
self.avatar = user.avatar
self.bot = user.bot
self._state = user._state
self._public_flags = user._public_flags
return self
@@ -272,11 +274,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.
@@ -317,17 +315,25 @@ class ClientUser(BaseUser):
.. versionadded:: 1.3
verified: :class:`bool`
Specifies if the user is a verified account.
Specifies if the user's email is verified.
email: Optional[:class:`str`]
The email the user used when registering.
.. deprecated:: 1.7
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).
.. deprecated:: 1.7
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.
.. deprecated:: 1.7
"""
__slots__ = BaseUser.__slots__ + \
('email', 'locale', '_flags', 'verified', 'mfa_enabled',
@@ -352,9 +358,12 @@ class ClientUser(BaseUser):
self.premium = data.get('premium', False)
self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
@deprecated()
def get_relationship(self, user_id):
"""Retrieves the :class:`Relationship` if applicable.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -375,6 +384,8 @@ class ClientUser(BaseUser):
def relationships(self):
"""List[:class:`User`]: Returns all the relationships that the user has.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -385,6 +396,8 @@ class ClientUser(BaseUser):
def friends(self):
r"""List[:class:`User`]: Returns all the users that the user is friends with.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -395,6 +408,8 @@ class ClientUser(BaseUser):
def blocked(self):
r"""List[:class:`User`]: Returns all the users that the user has blocked.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -409,6 +424,10 @@ class ClientUser(BaseUser):
If a bot account is used then a password field is optional,
otherwise it is required.
.. warning::
The user account-only fields are deprecated.
.. note::
To upload an avatar, a :term:`py:bytes-like object` must be passed in that
@@ -500,6 +519,7 @@ class ClientUser(BaseUser):
self._update(data)
@deprecated()
async def create_group(self, *recipients):
r"""|coro|
@@ -507,6 +527,8 @@ class ClientUser(BaseUser):
provided. These recipients must be have a relationship
of type :attr:`RelationshipType.friend`.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -540,11 +562,14 @@ class ClientUser(BaseUser):
data = await self._state.http.start_group(self.id, users)
return GroupChannel(me=self, data=data, state=self._state)
@deprecated()
async def edit_settings(self, **kwargs):
"""|coro|
Edits the client user's settings.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -706,8 +731,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.
@@ -729,17 +768,22 @@ class User(BaseUser, discord.abc.Messageable):
def relationship(self):
"""Optional[:class:`Relationship`]: Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
"""
return self._state.user.get_relationship(self.id)
@deprecated()
async def mutual_friends(self):
"""|coro|
Gets all mutual friends of this user.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -760,9 +804,12 @@ class User(BaseUser, discord.abc.Messageable):
mutuals = await state.http.get_mutual_friends(self.id)
return [User(state=state, data=friend) for friend in mutuals]
@deprecated()
def is_friend(self):
""":class:`bool`: Checks if the user is your friend.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -772,9 +819,12 @@ class User(BaseUser, discord.abc.Messageable):
return False
return r.type is RelationshipType.friend
@deprecated()
def is_blocked(self):
""":class:`bool`: Checks if the user is blocked.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -784,11 +834,14 @@ class User(BaseUser, discord.abc.Messageable):
return False
return r.type is RelationshipType.blocked
@deprecated()
async def block(self):
"""|coro|
Blocks the user.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -803,11 +856,14 @@ class User(BaseUser, discord.abc.Messageable):
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
@deprecated()
async def unblock(self):
"""|coro|
Unblocks the user.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -821,11 +877,14 @@ class User(BaseUser, discord.abc.Messageable):
"""
await self._state.http.remove_relationship(self.id)
@deprecated()
async def remove_friend(self):
"""|coro|
Removes the user as a friend.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -839,11 +898,14 @@ class User(BaseUser, discord.abc.Messageable):
"""
await self._state.http.remove_relationship(self.id)
@deprecated()
async def send_friend_request(self):
"""|coro|
Sends the user a friend request.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
@@ -857,11 +919,14 @@ class User(BaseUser, discord.abc.Messageable):
"""
await self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator)
@deprecated()
async def profile(self):
"""|coro|
Gets the user's profile.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -32,7 +32,7 @@ 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
@@ -110,6 +110,13 @@ def parse_time(timestamp):
return datetime.datetime(*map(int, re.split(r'[^\d]', timestamp.replace('+00:00', ''))))
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 +133,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 +148,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 = 'https://discord.com/oauth2/authorize?client_id={}'.format(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:
@@ -419,11 +431,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):
"""
@@ -482,6 +491,43 @@ _MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
_MARKDOWN_ESCAPE_REGEX = re.compile(r'(?P<markdown>%s|%s)' % (_MARKDOWN_ESCAPE_SUBREGEX, _MARKDOWN_ESCAPE_COMMON), re.MULTILINE)
_URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
_MARKDOWN_STOCK_REGEX = r'(?P<markdown>[_\\~|\*`]|%s)' % _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 = '(?:%s|%s)' % (_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 +554,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 +561,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 = '(?:%s|%s)' % (_URL_REGEX, regex)
return re.sub(regex, replacement, text, 0, re.MULTILINE)
else:
text = re.sub(r'\\', r'\\\\', text)
@@ -547,4 +592,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)

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -70,7 +70,7 @@ class VoiceProtocol:
These classes are passed to :meth:`abc.Connectable.connect`.
.. _Lavalink: https://github.com/Frederikam/Lavalink
.. _Lavalink: https://github.com/freyacodes/Lavalink
Parameters
------------

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -203,13 +203,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 +210,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
@@ -508,7 +509,7 @@ class WebhookMessage(Message):
"""
if delay is not None:
if self._state.parent._adapter.is_async():
if self._state._webhook._adapter.is_async():
return self._delete_delay_async(delay)
else:
return self._delete_delay_sync(delay)
@@ -687,7 +688,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()

View File

@@ -3,7 +3,7 @@
"""
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"),
@@ -156,7 +156,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.