looks like I changed stuff?

This commit is contained in:
iDutchy 2021-06-25 20:29:27 -05:00
commit 34f6c5db10
78 changed files with 2136 additions and 2539 deletions

View File

@ -10,7 +10,7 @@ sphinx:
builder: html builder: html
python: python:
version: 3.7 version: 3.8
install: install:
- method: pip - method: pip
path: . path: .

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
Discord API Wrapper Discord API Wrapper
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -15,53 +13,60 @@ __title__ = 'discord'
__author__ = 'Rapptz' __author__ = 'Rapptz'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz' __copyright__ = 'Copyright 2015-present Rapptz'
<<<<<<< HEAD
__version__ = '1.7.1.7' __version__ = '1.7.1.7'
=======
__version__ = '2.0.0.7a'
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
__path__ = __import__('pkgutil').extend_path(__path__, __name__) __path__ = __import__('pkgutil').extend_path(__path__, __name__)
from collections import namedtuple from collections import namedtuple
import logging import logging
from .client import Client from .client import *
from .appinfo import AppInfo from .appinfo import *
from .user import User, ClientUser, Profile from .user import *
from .emoji import Emoji from .emoji import *
from .partial_emoji import PartialEmoji from .partial_emoji import *
from .activity import * from .activity import *
from .channel import * from .channel import *
from .guild import Guild from .guild import *
from .flags import * from .flags import *
from .relationship import Relationship from .member import *
from .member import Member, VoiceState
from .message import * from .message import *
from .asset import Asset from .asset import *
from .errors import * from .errors import *
from .calls import CallMessage, GroupCall from .permissions import *
from .permissions import Permissions, PermissionOverwrite from .role import *
from .role import Role, RoleTags from .file import *
from .file import File from .colour import *
from .colour import Color, Colour from .integrations import *
from .integrations import Integration, IntegrationAccount from .invite import *
from .invite import Invite, PartialInviteChannel, PartialInviteGuild from .template import *
from .template import Template from .widget import *
from .widget import Widget, WidgetMember, WidgetChannel from .object import *
from .object import Object from .reaction import *
from .reaction import Reaction
from . import utils, opus, abc from . import utils, opus, abc
from .enums import * from .enums import *
from .embeds import Embed from .embeds import *
from .mentions import AllowedMentions from .mentions import *
from .shard import AutoShardedClient, ShardInfo from .shard import *
from .player import * from .player import *
from .webhook import * from .webhook import *
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import *
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff from .audit_logs import *
from .raw_models import * from .raw_models import *
from .team import * from .team import *
from .sticker import Sticker from .sticker import *
from .interactions import *
VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial') VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial')
<<<<<<< HEAD
version_info = VersionInfo(major=1, minor=7, micro=1, enhanced=7, releaselevel='final', serial=0) version_info = VersionInfo(major=1, minor=7, micro=1, enhanced=7, releaselevel='final', serial=0)
=======
version_info = VersionInfo(major=2, minor=0, micro=0, enhanced=7, releaselevel='alpha', serial=0)
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -42,9 +40,9 @@ def show_version():
if version_info.releaselevel != 'final': if version_info.releaselevel != 'final':
pkg = pkg_resources.get_distribution('discord.py') pkg = pkg_resources.get_distribution('discord.py')
if pkg: if pkg:
entries.append(' - discord.py pkg_resources: v{0}'.format(pkg.version)) entries.append(f' - discord.py pkg_resources: v{pkg.version}')
entries.append('- aiohttp v{0.__version__}'.format(aiohttp)) entries.append(f'- aiohttp v{aiohttp.__version__}')
uname = platform.uname() uname = platform.uname()
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname)) entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
print('\n'.join(entries)) print('\n'.join(entries))
@ -54,7 +52,6 @@ def core(parser, args):
show_version() show_version()
bot_template = """#!/usr/bin/env python3 bot_template = """#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from discord.ext import commands from discord.ext import commands
import discord import discord
@ -110,9 +107,7 @@ var/
config.py config.py
""" """
cog_template = '''# -*- coding: utf-8 -*- cog_template = '''from discord.ext import commands
from discord.ext import commands
import discord import discord
class {name}(commands.Cog{attrs}): class {name}(commands.Cog{attrs}):
@ -200,7 +195,7 @@ def newbot(parser, args):
try: try:
new_directory.mkdir(exist_ok=True, parents=True) new_directory.mkdir(exist_ok=True, parents=True)
except OSError as exc: except OSError as exc:
parser.error('could not create our bot directory ({})'.format(exc)) parser.error(f'could not create our bot directory ({exc})')
cogs = new_directory / 'cogs' cogs = new_directory / 'cogs'
@ -209,27 +204,27 @@ def newbot(parser, args):
init = cogs / '__init__.py' init = cogs / '__init__.py'
init.touch() init.touch()
except OSError as exc: except OSError as exc:
print('warning: could not create cogs directory ({})'.format(exc)) print(f'warning: could not create cogs directory ({exc})')
try: try:
with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp: with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
fp.write('token = "place your token here"\ncogs = []\n') fp.write('token = "place your token here"\ncogs = []\n')
except OSError as exc: except OSError as exc:
parser.error('could not create config file ({})'.format(exc)) parser.error(f'could not create config file ({exc})')
try: try:
with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp: with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp:
base = 'Bot' if not args.sharded else 'AutoShardedBot' base = 'Bot' if not args.sharded else 'AutoShardedBot'
fp.write(bot_template.format(base=base, prefix=args.prefix)) fp.write(bot_template.format(base=base, prefix=args.prefix))
except OSError as exc: except OSError as exc:
parser.error('could not create bot file ({})'.format(exc)) parser.error(f'could not create bot file ({exc})')
if not args.no_git: if not args.no_git:
try: try:
with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp: with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
fp.write(gitignore_template) fp.write(gitignore_template)
except OSError as exc: except OSError as exc:
print('warning: could not create .gitignore file ({})'.format(exc)) print(f'warning: could not create .gitignore file ({exc})')
print('successfully made bot at', new_directory) print('successfully made bot at', new_directory)
@ -238,7 +233,7 @@ def newcog(parser, args):
try: try:
cog_dir.mkdir(exist_ok=True) cog_dir.mkdir(exist_ok=True)
except OSError as exc: except OSError as exc:
print('warning: could not create cogs directory ({})'.format(exc)) print(f'warning: could not create cogs directory ({exc})')
directory = cog_dir / to_path(parser, args.name) directory = cog_dir / to_path(parser, args.name)
directory = directory.with_suffix('.py') directory = directory.with_suffix('.py')
@ -257,12 +252,12 @@ def newcog(parser, args):
name = name.title() name = name.title()
if args.display_name: if args.display_name:
attrs += ', name="{}"'.format(args.display_name) attrs += f', name="{args.display_name}"'
if args.hide_commands: if args.hide_commands:
attrs += ', command_attrs=dict(hidden=True)' attrs += ', command_attrs=dict(hidden=True)'
fp.write(cog_template.format(name=name, extra=extra, attrs=attrs)) fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
except OSError as exc: except OSError as exc:
parser.error('could not create cog file ({})'.format(exc)) parser.error(f'could not create cog file ({exc})')
else: else:
print('successfully made cog at', directory) print('successfully made cog at', directory)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,10 +22,12 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
import abc from __future__ import annotations
import sys import sys
import copy import copy
import asyncio import asyncio
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
from .iterators import HistoryIterator from .iterators import HistoryIterator
from .context_managers import Typing from .context_managers import Typing
@ -41,13 +41,31 @@ from .file import File
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import VoiceClient, VoiceProtocol
from . import utils from . import utils
__all__ = (
'Snowflake',
'User',
'PrivateChannel',
'GuildChannel',
'Messageable',
'Connectable',
)
if TYPE_CHECKING:
from datetime import datetime
from .user import ClientUser
class _Undefined: class _Undefined:
def __repr__(self): def __repr__(self):
return 'see-below' return 'see-below'
_undefined = _Undefined() _undefined = _Undefined()
class Snowflake(metaclass=abc.ABCMeta):
@runtime_checkable
class Snowflake(Protocol):
"""An ABC that details the common operations on a Discord model. """An ABC that details the common operations on a Discord model.
Almost all :ref:`Discord models <discord_api_models>` meet this Almost all :ref:`Discord models <discord_api_models>` meet this
@ -62,27 +80,16 @@ class Snowflake(metaclass=abc.ABCMeta):
The model's unique ID. The model's unique ID.
""" """
__slots__ = () __slots__ = ()
id: int
@property @property
@abc.abstractmethod def created_at(self) -> datetime:
def created_at(self): """:class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
""":class:`datetime.datetime`: Returns the model's creation time as a naive datetime in UTC."""
raise NotImplementedError raise NotImplementedError
@classmethod
def __subclasshook__(cls, C):
if cls is Snowflake:
mro = C.__mro__
for attr in ('created_at', 'id'):
for base in mro:
if attr in base.__dict__:
break
else:
return NotImplemented
return True
return NotImplemented
class User(metaclass=abc.ABCMeta): @runtime_checkable
class User(Snowflake, Protocol):
"""An ABC that details the common operations on a Discord user. """An ABC that details the common operations on a Discord user.
The following implement this ABC: The following implement this ABC:
@ -106,35 +113,24 @@ class User(metaclass=abc.ABCMeta):
""" """
__slots__ = () __slots__ = ()
name: str
discriminator: str
avatar: Optional[str]
bot: bool
@property @property
@abc.abstractmethod def display_name(self) -> str:
def display_name(self):
""":class:`str`: Returns the user's display name.""" """:class:`str`: Returns the user's display name."""
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod def mention(self) -> str:
def mention(self):
""":class:`str`: Returns a string that allows you to mention the given user.""" """:class:`str`: Returns a string that allows you to mention the given user."""
raise NotImplementedError raise NotImplementedError
@classmethod
def __subclasshook__(cls, C):
if cls is User:
if Snowflake.__subclasshook__(C) is NotImplemented:
return NotImplemented
mro = C.__mro__ @runtime_checkable
for attr in ('display_name', 'mention', 'name', 'avatar', 'discriminator', 'bot'): class PrivateChannel(Snowflake, Protocol):
for base in mro:
if attr in base.__dict__:
break
else:
return NotImplemented
return True
return NotImplemented
class PrivateChannel(metaclass=abc.ABCMeta):
"""An ABC that details the common operations on a private Discord channel. """An ABC that details the common operations on a private Discord channel.
The following implement this ABC: The following implement this ABC:
@ -151,18 +147,8 @@ class PrivateChannel(metaclass=abc.ABCMeta):
""" """
__slots__ = () __slots__ = ()
@classmethod me: ClientUser
def __subclasshook__(cls, C):
if cls is PrivateChannel:
if Snowflake.__subclasshook__(C) is NotImplemented:
return NotImplemented
mro = C.__mro__
for base in mro:
if 'me' in base.__dict__:
return True
return NotImplemented
return NotImplemented
class _Overwrites: class _Overwrites:
__slots__ = ('id', 'allow', 'deny', 'type') __slots__ = ('id', 'allow', 'deny', 'type')
@ -181,7 +167,8 @@ class _Overwrites:
'type': self.type, 'type': self.type,
} }
class GuildChannel:
class GuildChannel(Protocol):
"""An ABC that details the common operations on a Discord guild channel. """An ABC that details the common operations on a Discord guild channel.
The following implement this ABC: The following implement this ABC:
@ -193,6 +180,11 @@ class GuildChannel:
This ABC must also implement :class:`~discord.abc.Snowflake`. This ABC must also implement :class:`~discord.abc.Snowflake`.
Note
----
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
checks.
Attributes Attributes
----------- -----------
name: :class:`str` name: :class:`str`
@ -365,7 +357,7 @@ class GuildChannel:
@property @property
def mention(self): def mention(self):
""":class:`str`: The string that allows you to mention the channel.""" """:class:`str`: The string that allows you to mention the channel."""
return '<#%s>' % self.id return f'<#{self.id}>'
@property @property
def created_at(self): def created_at(self):
@ -832,14 +824,13 @@ class GuildChannel:
lock_permissions = kwargs.get('sync_permissions', False) lock_permissions = kwargs.get('sync_permissions', False)
reason = kwargs.get('reason') reason = kwargs.get('reason')
for index, channel in enumerate(channels): for index, channel in enumerate(channels):
d = { 'id': channel.id, 'position': index } d = {'id': channel.id, 'position': index}
if parent_id is not ... and channel.id == self.id: if parent_id is not ... and channel.id == self.id:
d.update(parent_id=parent_id, lock_permissions=lock_permissions) d.update(parent_id=parent_id, lock_permissions=lock_permissions)
payload.append(d) payload.append(d)
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason) await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
async def create_invite(self, *, reason=None, **fields): async def create_invite(self, *, reason=None, **fields):
"""|coro| """|coro|
@ -914,7 +905,8 @@ class GuildChannel:
return result return result
class Messageable(metaclass=abc.ABCMeta):
class Messageable(Protocol):
"""An ABC that details the common operations on a model that can send messages. """An ABC that details the common operations on a model that can send messages.
The following implement this ABC: The following implement this ABC:
@ -925,11 +917,16 @@ class Messageable(metaclass=abc.ABCMeta):
- :class:`~discord.User` - :class:`~discord.User`
- :class:`~discord.Member` - :class:`~discord.Member`
- :class:`~discord.ext.commands.Context` - :class:`~discord.ext.commands.Context`
Note
----
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
checks.
""" """
__slots__ = () __slots__ = ()
@abc.abstractmethod
async def _get_channel(self): async def _get_channel(self):
raise NotImplementedError raise NotImplementedError
@ -1072,8 +1069,8 @@ class Messageable(metaclass=abc.ABCMeta):
f.close() f.close()
else: else:
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
nonce=nonce, allowed_mentions=allowed_mentions, nonce=nonce, allowed_mentions=allowed_mentions,
message_reference=reference) message_reference=reference)
ret = state.create_message(channel=channel, data=data) ret = state.create_message(channel=channel, data=data)
if delete_after is not None: if delete_after is not None:
@ -1198,13 +1195,16 @@ class Messageable(metaclass=abc.ABCMeta):
that this would make it a slow operation. that this would make it a slow operation.
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages before this date or message. Retrieve messages before this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages after this date or message. Retrieve messages after this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages around this date or message. Retrieve messages around this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
When using this argument, the maximum limit is 101. Note that if the limit is an When using this argument, the maximum limit is 101. Note that if the limit is an
even number then this will return at most limit + 1 messages. even number then this will return at most limit + 1 messages.
oldest_first: Optional[:class:`bool`] oldest_first: Optional[:class:`bool`]
@ -1225,21 +1225,25 @@ class Messageable(metaclass=abc.ABCMeta):
""" """
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first) return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
class Connectable(metaclass=abc.ABCMeta):
class Connectable(Protocol):
"""An ABC that details the common operations on a channel that can """An ABC that details the common operations on a channel that can
connect to a voice server. connect to a voice server.
The following implement this ABC: The following implement this ABC:
- :class:`~discord.VoiceChannel` - :class:`~discord.VoiceChannel`
Note
----
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
checks.
""" """
__slots__ = () __slots__ = ()
@abc.abstractmethod
def _get_voice_client_key(self): def _get_voice_client_key(self):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def _get_voice_state_pair(self): def _get_voice_state_pair(self):
raise NotImplementedError raise NotImplementedError
@ -1298,6 +1302,6 @@ class Connectable(metaclass=abc.ABCMeta):
except Exception: except Exception:
# we don't care if disconnect failed because connection failed # we don't care if disconnect failed because connection failed
pass pass
raise # re-raise raise # re-raise
return voice return voice

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -187,7 +185,10 @@ class Activity(BaseActivity):
self.flags = kwargs.pop('flags', 0) self.flags = kwargs.pop('flags', 0)
self.sync_id = kwargs.pop('sync_id', None) self.sync_id = kwargs.pop('sync_id', None)
self.session_id = kwargs.pop('session_id', None) self.session_id = kwargs.pop('session_id', None)
self.type = try_enum(ActivityType, kwargs.pop('type', -1))
activity_type = kwargs.pop('type', -1)
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
emoji = kwargs.pop('emoji', None) emoji = kwargs.pop('emoji', None)
if emoji is not None: if emoji is not None:
self.emoji = PartialEmoji.from_dict(emoji) self.emoji = PartialEmoji.from_dict(emoji)
@ -196,16 +197,16 @@ class Activity(BaseActivity):
def __repr__(self): def __repr__(self):
attrs = ( attrs = (
'type', ('type', self.type),
'name', ('name', self.name),
'url', ('url', self.url),
'details', ('details', self.details),
'application_id', ('application_id', self.application_id),
'session_id', ('session_id', self.session_id),
'emoji', ('emoji', self.emoji),
) )
mapped = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in attrs) inner = ' '.join('%s=%r' % t for t in attrs)
return '<Activity %s>' % mapped return f'<Activity {inner}>'
def to_dict(self): def to_dict(self):
ret = {} ret = {}
@ -227,17 +228,21 @@ class Activity(BaseActivity):
def start(self): def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
try: try:
return datetime.datetime.utcfromtimestamp(self.timestamps['start'] / 1000) timestamp = self.timestamps['start'] / 1000
except KeyError: except KeyError:
return None return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
@property @property
def end(self): def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
try: try:
return datetime.datetime.utcfromtimestamp(self.timestamps['end'] / 1000) timestamp = self.timestamps['end'] / 1000
except KeyError: except KeyError:
return None return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
@property @property
def large_image_url(self): def large_image_url(self):
@ -250,7 +255,7 @@ class Activity(BaseActivity):
except KeyError: except KeyError:
return None return None
else: else:
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, large_image) return Asset.BASE + f'/app-assets/{self.application_id}/{large_image}.png'
@property @property
def small_image_url(self): def small_image_url(self):
@ -263,7 +268,7 @@ class Activity(BaseActivity):
except KeyError: except KeyError:
return None return None
else: else:
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, small_image) return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
@property @property
def large_image_text(self): def large_image_text(self):
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable.""" """Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
@ -302,10 +307,6 @@ class Game(BaseActivity):
----------- -----------
name: :class:`str` name: :class:`str`
The game's name. The game's name.
start: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
end: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
Attributes Attributes
----------- -----------
@ -322,20 +323,12 @@ class Game(BaseActivity):
try: try:
timestamps = extra['timestamps'] timestamps = extra['timestamps']
except KeyError: except KeyError:
self._extract_timestamp(extra, 'start') self._start = 0
self._extract_timestamp(extra, 'end') self._end = 0
else: else:
self._start = timestamps.get('start', 0) self._start = timestamps.get('start', 0)
self._end = timestamps.get('end', 0) self._end = timestamps.get('end', 0)
def _extract_timestamp(self, data, key):
try:
dt = data[key]
except KeyError:
setattr(self, '_' + key, 0)
else:
setattr(self, '_' + key, dt.timestamp() * 1000.0)
@property @property
def type(self): def type(self):
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
@ -348,21 +341,21 @@ class Game(BaseActivity):
def start(self): def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
if self._start: if self._start:
return datetime.datetime.utcfromtimestamp(self._start / 1000) return datetime.datetime.utcfromtimestamp(self._start / 1000).replace(tzinfo=datetime.timezone.utc)
return None return None
@property @property
def end(self): def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
if self._end: if self._end:
return datetime.datetime.utcfromtimestamp(self._end / 1000) return datetime.datetime.utcfromtimestamp(self._end / 1000).replace(tzinfo=datetime.timezone.utc)
return None return None
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
def __repr__(self): def __repr__(self):
return '<Game name={0.name!r}>'.format(self) return f'<Game name={self.name!r}>'
def to_dict(self): def to_dict(self):
timestamps = {} timestamps = {}
@ -455,7 +448,7 @@ class Streaming(BaseActivity):
return str(self.name) return str(self.name)
def __repr__(self): def __repr__(self):
return '<Streaming name={0.name!r}>'.format(self) return f'<Streaming name={self.name!r}>'
@property @property
def twitch_name(self): def twitch_name(self):
@ -700,7 +693,7 @@ class CustomActivity(BaseActivity):
elif isinstance(emoji, PartialEmoji): elif isinstance(emoji, PartialEmoji):
self.emoji = emoji self.emoji = emoji
else: else:
raise TypeError('Expected str, PartialEmoji, or None, received {0!r} instead.'.format(type(emoji))) raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
@property @property
def type(self): def type(self):
@ -739,7 +732,7 @@ class CustomActivity(BaseActivity):
def __str__(self): def __str__(self):
if self.emoji: if self.emoji:
if self.name: if self.name:
return '%s %s' % (self.emoji, self.name) return f'{self.emoji} {self.name}'
return str(self.emoji) return str(self.emoji)
else: else:
return str(self.name) return str(self.name)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -29,6 +27,9 @@ from .user import User
from .asset import Asset from .asset import Asset
from .team import Team from .team import Team
__all__ = (
'AppInfo',
)
class AppInfo: class AppInfo:
"""Represents the application info for the bot provided by Discord. """Represents the application info for the bot provided by Discord.

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -29,6 +27,10 @@ from .errors import DiscordException
from .errors import InvalidArgument from .errors import InvalidArgument
from . import utils from . import utils
__all__ = (
'Asset',
)
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"} VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
@ -74,11 +76,11 @@ class Asset:
if not utils.valid_icon_size(size): if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096") raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format is not None and format not in VALID_AVATAR_FORMATS: if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS)) raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not user.is_avatar_animated(): if format == "gif" and not user.is_avatar_animated():
raise InvalidArgument("non animated avatars do not support gif format") raise InvalidArgument("non animated avatars do not support gif format")
if static_format not in VALID_STATIC_FORMATS: if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if user.avatar is None: if user.avatar is None:
return user.default_avatar_url return user.default_avatar_url
@ -96,7 +98,7 @@ class Asset:
if not utils.valid_icon_size(size): if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096") raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS: if format not in VALID_STATIC_FORMATS:
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size) url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
return cls(state, url) return cls(state, url)
@ -109,7 +111,7 @@ class Asset:
if not utils.valid_icon_size(size): if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096") raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS: if format not in VALID_STATIC_FORMATS:
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size) url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
return cls(state, url) return cls(state, url)
@ -119,7 +121,7 @@ class Asset:
if not utils.valid_icon_size(size): if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096") raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS: if format not in VALID_STATIC_FORMATS:
raise InvalidArgument("format must be one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
if hash is None: if hash is None:
return cls(state) return cls(state)
@ -132,11 +134,11 @@ class Asset:
if not utils.valid_icon_size(size): if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096") raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format is not None and format not in VALID_AVATAR_FORMATS: if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument("format must be one of {}".format(VALID_AVATAR_FORMATS)) raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not guild.is_icon_animated(): if format == "gif" and not guild.is_icon_animated():
raise InvalidArgument("non animated guild icons do not support gif format") raise InvalidArgument("non animated guild icons do not support gif format")
if static_format not in VALID_STATIC_FORMATS: if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if guild.icon is None: if guild.icon is None:
return cls(state) return cls(state)
@ -156,15 +158,15 @@ class Asset:
@classmethod @classmethod
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'): def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
if format is not None and format not in VALID_AVATAR_FORMATS: if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS)) raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not emoji.animated: if format == "gif" and not emoji.animated:
raise InvalidArgument("non animated emoji's do not support gif format") raise InvalidArgument("non animated emoji's do not support gif format")
if static_format not in VALID_STATIC_FORMATS: if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS)) raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if format is None: if format is None:
format = 'gif' if emoji.animated else static_format format = 'gif' if emoji.animated else static_format
return cls(state, '/emojis/{0.id}.{1}'.format(emoji, format)) return cls(state, f'/emojis/{emoji.id}.{format}')
def __str__(self): def __str__(self):
return self.BASE + self._url if self._url is not None else '' return self.BASE + self._url if self._url is not None else ''
@ -178,7 +180,7 @@ class Asset:
return self._url is not None return self._url is not None
def __repr__(self): def __repr__(self):
return '<Asset url={0._url!r}>'.format(self) return f'<Asset url={self._url!r}>'
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, Asset) and self._url == other._url return isinstance(other, Asset) and self._url == other._url

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -31,6 +29,12 @@ from .colour import Colour
from .invite import Invite from .invite import Invite
from .mixins import Hashable from .mixins import Hashable
__all__ = (
'AuditLogDiff',
'AuditLogChanges',
'AuditLogEntry',
)
def _transform_verification_level(entry, data): def _transform_verification_level(entry, data):
return enums.try_enum(enums.VerificationLevel, data) return enums.try_enum(enums.VerificationLevel, data)
@ -94,7 +98,7 @@ class AuditLogDiff:
def __repr__(self): def __repr__(self):
values = ' '.join('%s=%r' % item for item in self.__dict__.items()) values = ' '.join('%s=%r' % item for item in self.__dict__.items())
return '<AuditLogDiff %s>' % values return f'<AuditLogDiff {values}>'
class AuditLogChanges: class AuditLogChanges:
TRANSFORMERS = { TRANSFORMERS = {
@ -166,7 +170,7 @@ class AuditLogChanges:
self.before.color = self.before.colour self.before.color = self.before.colour
def __repr__(self): def __repr__(self):
return '<AuditLogChanges before=%r after=%r>' % (self.before, self.after) return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
def _handle_role(self, first, second, entry, elem): def _handle_role(self, first, second, entry, elem):
if not hasattr(first, 'roles'): if not hasattr(first, 'roles'):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
import time import time
import random import random
__all__ = (
'ExponentialBackoff',
)
class ExponentialBackoff: class ExponentialBackoff:
"""An implementation of the exponential backoff algorithm """An implementation of the exponential backoff algorithm

View File

@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import datetime
from . import utils
from .enums import VoiceRegion, try_enum
from .member import VoiceState
class CallMessage:
"""Represents a group call message from Discord.
This is only received in cases where the message type is equivalent to
:attr:`MessageType.call`.
.. deprecated:: 1.7
Attributes
-----------
ended_timestamp: Optional[:class:`datetime.datetime`]
A naive UTC datetime object that represents the time that the call has ended.
participants: List[:class:`User`]
The list of users that are participating in this call.
message: :class:`Message`
The message associated with this call message.
"""
def __init__(self, message, **kwargs):
self.message = message
self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp'))
self.participants = kwargs.get('participants')
@property
def call_ended(self):
""":class:`bool`: Indicates if the call has ended.
.. deprecated:: 1.7
"""
return self.ended_timestamp is not None
@property
def channel(self):
r""":class:`GroupChannel`\: The private channel associated with this message.
.. deprecated:: 1.7
"""
return self.message.channel
@property
def duration(self):
"""Queries the duration of the call.
If the call has not ended then the current duration will
be returned.
.. deprecated:: 1.7
Returns
---------
:class:`datetime.timedelta`
The timedelta object representing the duration.
"""
if self.ended_timestamp is None:
return datetime.datetime.utcnow() - self.message.created_at
else:
return self.ended_timestamp - self.message.created_at
class GroupCall:
"""Represents the actual group call from Discord.
This is accompanied with a :class:`CallMessage` denoting the information.
.. deprecated:: 1.7
Attributes
-----------
call: :class:`CallMessage`
The call message associated with this group call.
unavailable: :class:`bool`
Denotes if this group call is unavailable.
ringing: List[:class:`User`]
A list of users that are currently being rung to join the call.
region: :class:`VoiceRegion`
The guild region the group call is being hosted on.
"""
def __init__(self, **kwargs):
self.call = kwargs.get('call')
self.unavailable = kwargs.get('unavailable')
self._voice_states = {}
for state in kwargs.get('voice_states', []):
self._update_voice_state(state)
self._update(**kwargs)
def _update(self, **kwargs):
self.region = try_enum(VoiceRegion, kwargs.get('region'))
lookup = {u.id: u for u in self.call.channel.recipients}
me = self.call.channel.me
lookup[me.id] = me
self.ringing = list(filter(None, map(lookup.get, kwargs.get('ringing', []))))
def _update_voice_state(self, data):
user_id = int(data['user_id'])
# left the voice channel?
if data['channel_id'] is None:
self._voice_states.pop(user_id, None)
else:
self._voice_states[user_id] = VoiceState(data=data, channel=self.channel)
@property
def connected(self):
"""List[:class:`User`]: A property that returns all users that are currently in this call.
.. deprecated:: 1.7
"""
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
me = self.channel.me
if self.voice_state_for(me) is not None:
ret.append(me)
return ret
@property
def channel(self):
r""":class:`GroupChannel`\: Returns the channel the group call is in.
.. deprecated:: 1.7
"""
return self.call.channel
@utils.deprecated()
def voice_state_for(self, user):
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
If the :class:`User` has no voice state then this function returns
``None``.
.. deprecated:: 1.7
Parameters
------------
user: :class:`User`
The user to retrieve the voice state for.
Returns
--------
Optional[:class:`VoiceState`]
The voice state associated with this user.
"""
return self._voice_states.get(user.id)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -115,7 +113,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
('news', self.is_news()), ('news', self.is_news()),
('category_id', self.category_id) ('category_id', self.category_id)
] ]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs)) joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
def _update(self, guild, data): def _update(self, guild, data):
self.guild = guild self.guild = guild
@ -319,10 +318,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
account). The :attr:`~Permissions.read_message_history` permission is account). The :attr:`~Permissions.read_message_history` permission is
also needed to retrieve message history. also needed to retrieve message history.
Internally, this employs a different number of strategies depending
on the conditions met such as if a bulk delete is possible or if
the account is a user bot or not.
Examples Examples
--------- ---------
@ -353,8 +348,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
bulk: :class:`bool` bulk: :class:`bool`
If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting 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 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 (now deprecated), or if messages are fall back to single delete if messages are older than two weeks.
older than two weeks.
Raises Raises
------- -------
@ -377,7 +371,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
count = 0 count = 0
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
strategy = self.delete_messages if self._state.is_bot and bulk else _single_delete_strategy strategy = self.delete_messages if bulk else _single_delete_strategy
while True: while True:
try: try:
@ -675,7 +669,8 @@ class VoiceChannel(VocalGuildChannel):
('user_limit', self.user_limit), ('user_limit', self.user_limit),
('category_id', self.category_id) ('category_id', self.category_id)
] ]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs)) joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
@property @property
def type(self): def type(self):
@ -798,7 +793,8 @@ class StageChannel(VocalGuildChannel):
('user_limit', self.user_limit), ('user_limit', self.user_limit),
('category_id', self.category_id) ('category_id', self.category_id)
] ]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs)) joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
def _update(self, guild, data): def _update(self, guild, data):
super()._update(guild, data) super()._update(guild, data)
@ -1226,7 +1222,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
return self return self
def __str__(self): def __str__(self):
return 'Direct Message with %s' % self.recipient return f'Direct Message with {self.recipient}'
def __repr__(self): def __repr__(self):
return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self) return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self)
@ -1448,95 +1444,6 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return base return base
@utils.deprecated()
async def add_recipients(self, *recipients):
r"""|coro|
Adds recipients to this group.
A group can only have a maximum of 10 members.
Attempting to add more ends up in an exception. To
add a recipient to the group, you must have a relationship
with the user of type :attr:`RelationshipType.friend`.
.. deprecated:: 1.7
Parameters
-----------
\*recipients: :class:`User`
An argument list of users to add to this group.
Raises
-------
HTTPException
Adding a recipient to this group failed.
"""
# TODO: wait for the corresponding WS event
req = self._state.http.add_group_recipient
for recipient in recipients:
await req(self.id, recipient.id)
@utils.deprecated()
async def remove_recipients(self, *recipients):
r"""|coro|
Removes recipients from this group.
.. deprecated:: 1.7
Parameters
-----------
\*recipients: :class:`User`
An argument list of users to remove from this group.
Raises
-------
HTTPException
Removing a recipient from this group failed.
"""
# TODO: wait for the corresponding WS event
req = self._state.http.remove_group_recipient
for recipient in recipients:
await req(self.id, recipient.id)
@utils.deprecated()
async def edit(self, **fields):
"""|coro|
Edits the group.
.. deprecated:: 1.7
Parameters
-----------
name: Optional[:class:`str`]
The new name to change the group to.
Could be ``None`` to remove the name.
icon: Optional[:class:`bytes`]
A :term:`py:bytes-like object` representing the new icon.
Could be ``None`` to remove the icon.
Raises
-------
HTTPException
Editing the group failed.
"""
try:
icon_bytes = fields['icon']
except KeyError:
pass
else:
if icon_bytes is not None:
fields['icon'] = utils._bytes_to_base64_data(icon_bytes)
data = await self._state.http.edit_group(self.id, **fields)
self._update_group(data)
async def leave(self): async def leave(self):
"""|coro| """|coro|

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -34,7 +32,7 @@ import re
import aiohttp import aiohttp
from .user import User, Profile from .user import User
from .invite import Invite from .invite import Invite
from .template import Template from .template import Template
from .widget import Widget from .widget import Widget
@ -57,16 +55,14 @@ from .iterators import GuildIterator
from .appinfo import AppInfo from .appinfo import AppInfo
from .colour import Color, Colour from .colour import Color, Colour
__all__ = (
'Client',
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _cancel_tasks(loop): def _cancel_tasks(loop):
try: tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()}
task_retriever = asyncio.Task.all_tasks
except AttributeError:
# future proofing for 3.9 I guess
task_retriever = asyncio.all_tasks
tasks = {t for t in task_retriever(loop=loop) if not t.done()}
if not tasks: if not tasks:
return return
@ -91,28 +87,11 @@ def _cancel_tasks(loop):
def _cleanup_loop(loop): def _cleanup_loop(loop):
try: try:
_cancel_tasks(loop) _cancel_tasks(loop)
if sys.version_info >= (3, 6): loop.run_until_complete(loop.shutdown_asyncgens())
loop.run_until_complete(loop.shutdown_asyncgens())
finally: finally:
log.info('Closing the event loop.') log.info('Closing the event loop.')
loop.close() loop.close()
class _ClientEventTask(asyncio.Task):
def __init__(self, original_coro, event_name, coro, *, loop):
super().__init__(coro, loop=loop)
self.__event_name = event_name
self.__original_coro = original_coro
def __repr__(self):
info = [
('state', self._state.lower()),
('event', self.__event_name),
('coro', repr(self.__original_coro)),
]
if self._exception is not None:
info.append(('exception', repr(self._exception)))
return '<ClientEventTask {}>'.format(' '.join('%s=%s' % t for t in info))
class Client: class Client:
r"""Represents a client connection that connects to Discord. r"""Represents a client connection that connects to Discord.
This class is used to interact with the Discord WebSocket and API. This class is used to interact with the Discord WebSocket and API.
@ -141,6 +120,8 @@ class Client:
Integer starting at ``0`` and less than :attr:`.shard_count`. Integer starting at ``0`` and less than :attr:`.shard_count`.
shard_count: Optional[:class:`int`] shard_count: Optional[:class:`int`]
The total number of shards. The total number of shards.
application_id: :class:`int`
The client's application ID.
intents: :class:`Intents` intents: :class:`Intents`
The intents that you want to enable for the session. This is a way of The intents that you want to enable for the session. This is a way of
disabling and enabling certain gateway events from triggering and being sent. disabling and enabling certain gateway events from triggering and being sent.
@ -344,10 +325,7 @@ class Client:
def _get_state(self, **options): def _get_state(self, **options):
return ConnectionState(dispatch=self.dispatch, handlers=self._handlers, return ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options) hooks=self._hooks, http=self.http, loop=self.loop, **options)
async def _syncer(self, guilds):
await self.ws.request_sync(guilds)
def _handle_ready(self): def _handle_ready(self):
self._ready.set() self._ready.set()
@ -415,6 +393,16 @@ class Client:
""" """
return self._connection.voice_clients return self._connection.voice_clients
@property
def application_id(self):
"""Optional[:class:`int`]: The client's application ID.
If this is not passed via ``__init__`` then this is retrieved
through the gateway when an event contains the data. Usually
after :func:`on_connect` is called.
"""
return self._connection.application_id
def is_ready(self): def is_ready(self):
""":class:`bool`: Specifies if the client's internal cache is ready for use.""" """:class:`bool`: Specifies if the client's internal cache is ready for use."""
return self._ready.is_set() return self._ready.is_set()
@ -433,7 +421,7 @@ class Client:
def _schedule_event(self, coro, event_name, *args, **kwargs): def _schedule_event(self, coro, event_name, *args, **kwargs):
wrapped = self._run_event(coro, event_name, *args, **kwargs) wrapped = self._run_event(coro, event_name, *args, **kwargs)
# Schedules the task # Schedules the task
return _ClientEventTask(original_coro=coro, event_name=event_name, coro=wrapped, loop=self.loop) return asyncio.create_task(wrapped, name=f'discord.py: {event_name}')
def dispatch(self, event, *args, **kwargs): def dispatch(self, event, *args, **kwargs):
log.debug('Dispatching event %s', event) log.debug('Dispatching event %s', event)
@ -484,43 +472,9 @@ class Client:
overridden to have a different implementation. overridden to have a different implementation.
Check :func:`~discord.on_error` for more details. Check :func:`~discord.on_error` for more details.
""" """
print('Ignoring exception in {}'.format(event_method), file=sys.stderr) print(f'Ignoring exception in {event_method}', file=sys.stderr)
traceback.print_exc() traceback.print_exc()
@utils.deprecated('Guild.chunk')
async def request_offline_members(self, *guilds):
r"""|coro|
Requests previously offline members from the guild to be filled up
into the :attr:`.Guild.members` cache. This function is usually not
called. It should only be used if you have the ``fetch_offline_members``
parameter set to ``False``.
When the client logs on and connects to the websocket, Discord does
not provide the library with offline members if the number of members
in the guild is larger than 250. You can check if a guild is large
if :attr:`.Guild.large` is ``True``.
.. warning::
This method is deprecated. Use :meth:`Guild.chunk` instead.
Parameters
-----------
\*guilds: :class:`.Guild`
An argument list of guilds to request offline members for.
Raises
-------
:exc:`.InvalidArgument`
If any guild is unavailable in the collection.
"""
if any(g.unavailable for g in guilds):
raise InvalidArgument('An unavailable guild was passed.')
for guild in guilds:
await self._connection.chunk_guild(guild)
# hooks # hooks
async def _call_before_identify_hook(self, shard_id, *, initial=False): async def _call_before_identify_hook(self, shard_id, *, initial=False):
@ -553,7 +507,7 @@ class Client:
# login state management # login state management
async def login(self, token, *, bot=True): async def login(self, token):
"""|coro| """|coro|
Logs in the client with the specified credentials. Logs in the client with the specified credentials.
@ -572,11 +526,6 @@ class Client:
token: :class:`str` token: :class:`str`
The authentication token. Do not prefix this token with The authentication token. Do not prefix this token with
anything as the library will do it for you. anything as the library will do it for you.
bot: :class:`bool`
Keyword argument that specifies if the account logging on is a bot
token or not.
.. deprecated:: 1.7
Raises Raises
------ ------
@ -589,24 +538,7 @@ class Client:
""" """
log.info('logging in using static token') log.info('logging in using static token')
await self.http.static_login(token.strip(), bot=bot) await self.http.static_login(token.strip())
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::
This is just an alias to :meth:`close`. If you want
to do extraneous cleanup when subclassing, it is suggested
to override :meth:`close` instead.
"""
await self.close()
async def connect(self, *, reconnect=True): async def connect(self, *, reconnect=True):
"""|coro| """|coro|
@ -727,7 +659,7 @@ class Client:
self._connection.clear() self._connection.clear()
self.http.recreate() self.http.recreate()
async def start(self, *args, **kwargs): async def start(self, token, *, reconnect=True):
"""|coro| """|coro|
A shorthand coroutine for :meth:`login` + :meth:`connect`. A shorthand coroutine for :meth:`login` + :meth:`connect`.
@ -737,13 +669,7 @@ class Client:
TypeError TypeError
An unexpected keyword argument was received. An unexpected keyword argument was received.
""" """
bot = kwargs.pop('bot', True) await self.login(token)
reconnect = kwargs.pop('reconnect', True)
if kwargs:
raise TypeError("unexpected keyword argument(s) %s" % list(kwargs.keys()))
await self.login(*args, bot=bot)
await self.connect(reconnect=reconnect) await self.connect(reconnect=reconnect)
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
@ -841,7 +767,7 @@ class Client:
if value is None or isinstance(value, AllowedMentions): if value is None or isinstance(value, AllowedMentions):
self._connection.allowed_mentions = value self._connection.allowed_mentions = value
else: else:
raise TypeError('allowed_mentions must be AllowedMentions not {0.__class__!r}'.format(value)) raise TypeError(f'allowed_mentions must be AllowedMentions not {value.__class__!r}')
@property @property
def intents(self): def intents(self):
@ -940,8 +866,7 @@ class Client:
""" """
for guild in self.guilds: for guild in self.guilds:
for channel in guild.channels: yield from guild.channels
yield channel
def get_all_members(self): def get_all_members(self):
"""Returns a generator with every :class:`.Member` the client can see. """Returns a generator with every :class:`.Member` the client can see.
@ -958,8 +883,7 @@ class Client:
A member the client can see. A member the client can see.
""" """
for guild in self.guilds: for guild in self.guilds:
for member in guild.members: yield from guild.members
yield member
# listeners/waiters # listeners/waiters
@ -1192,10 +1116,12 @@ class Client:
Defaults to ``100``. Defaults to ``100``.
before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieves guilds before this date or object. Retrieves guilds before this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieve guilds after this date or object. Retrieve guilds after this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
Raises Raises
------ ------
@ -1501,51 +1427,6 @@ class Client:
data = await self.http.get_user(user_id) data = await self.http.get_user(user_id)
return User(state=self._connection, data=data) 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.
Parameters
------------
user_id: :class:`int`
The ID of the user to fetch their profile for.
Raises
-------
:exc:`.Forbidden`
Not allowed to fetch profiles.
:exc:`.HTTPException`
Fetching the profile failed.
Returns
--------
:class:`.Profile`
The profile of the user.
"""
state = self._connection
data = await self.http.get_user_profile(user_id)
def transform(d):
return state._get_guild(int(d['id']))
since = data.get('premium_since')
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
user = data['user']
return Profile(flags=user.get('flags', 0),
premium_since=utils.parse_time(since),
mutual_guilds=mutual_guilds,
user=User(data=user, state=state),
connected_accounts=data['connected_accounts'])
async def fetch_channel(self, channel_id): async def fetch_channel(self, channel_id):
"""|coro| """|coro|

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,11 @@ DEALINGS IN THE SOFTWARE.
import colorsys import colorsys
import random import random
__all__ = (
'Colour',
'Color',
)
class Colour: class Colour:
"""Represents a Discord role colour. This class is similar """Represents a Discord role colour. This class is similar
to a (red, green, blue) :class:`tuple`. to a (red, green, blue) :class:`tuple`.
@ -61,7 +64,7 @@ class Colour:
def __init__(self, value): def __init__(self, value):
if not isinstance(value, int): if not isinstance(value, int):
raise TypeError('Expected int parameter, received %s instead.' % value.__class__.__name__) raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
self.value = value self.value = value
@ -75,10 +78,10 @@ class Colour:
return not self.__eq__(other) return not self.__eq__(other)
def __str__(self): def __str__(self):
return '#{:0>6x}'.format(self.value) return f'#{self.value:0>6x}'
def __repr__(self): def __repr__(self):
return '<Colour value=%s>' % self.value return f'<Colour value={self.value}>'
def __hash__(self): def __hash__(self):
return hash(self.value) return hash(self.value)
@ -138,7 +141,7 @@ class Colour:
Parameters Parameters
------------ ------------
seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]] 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. The seed to initialize the RNG with. If ``None`` is passed the default RNG is used.
.. versionadded:: 1.7 .. versionadded:: 1.7
""" """

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
import asyncio import asyncio
__all__ = (
'Typing',
)
def _typing_done_callback(fut): def _typing_done_callback(fut):
# just retrieve any exception and call it a day # just retrieve any exception and call it a day
try: try:

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,37 +22,88 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations
import datetime import datetime
import os import os
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
from . import utils from . import utils
from .colour import Colour from .colour import Colour
__all__ = (
'Embed',
)
class _EmptyEmbed: class _EmptyEmbed:
def __bool__(self): def __bool__(self) -> bool:
return False return False
def __repr__(self): def __repr__(self) -> str:
return 'Embed.Empty' return 'Embed.Empty'
def __len__(self): def __len__(self) -> int:
return 0 return 0
EmptyEmbed = _EmptyEmbed()
EmptyEmbed: Final = _EmptyEmbed()
class EmbedProxy: class EmbedProxy:
def __init__(self, layer): def __init__(self, layer: Dict[str, Any]):
self.__dict__.update(layer) self.__dict__.update(layer)
def __len__(self): def __len__(self) -> int:
return len(self.__dict__) return len(self.__dict__)
def __repr__(self): def __repr__(self) -> str:
return 'EmbedProxy(%s)' % ', '.join(('%s=%r' % (k, v) for k, v in self.__dict__.items() if not k.startswith('_'))) inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
return f'EmbedProxy({inner})'
def __getattr__(self, attr): def __getattr__(self, attr: str) -> _EmptyEmbed:
return EmptyEmbed return EmptyEmbed
E = TypeVar('E', bound='Embed')
if TYPE_CHECKING:
from discord.types.embed import Embed as EmbedData, EmbedType
T = TypeVar('T')
MaybeEmpty = Union[T, _EmptyEmbed]
class _EmbedFooterProxy(Protocol):
text: MaybeEmpty[str]
icon_url: MaybeEmpty[str]
class _EmbedFieldProxy(Protocol):
name: MaybeEmpty[str]
value: MaybeEmpty[str]
inline: bool
class _EmbedMediaProxy(Protocol):
url: MaybeEmpty[str]
proxy_url: MaybeEmpty[str]
height: MaybeEmpty[int]
width: MaybeEmpty[int]
class _EmbedVideoProxy(Protocol):
url: MaybeEmpty[str]
height: MaybeEmpty[int]
width: MaybeEmpty[int]
class _EmbedProviderProxy(Protocol):
name: MaybeEmpty[str]
url: MaybeEmpty[str]
class _EmbedAuthorProxy(Protocol):
name: MaybeEmpty[str]
url: MaybeEmpty[str]
icon_url: MaybeEmpty[str]
proxy_icon_url: MaybeEmpty[str]
class Embed: class Embed:
"""Represents a Discord embed. """Represents a Discord embed.
@ -65,6 +114,12 @@ class Embed:
Returns the total size of the embed. Returns the total size of the embed.
Useful for checking if it's within the 6000 character limit. Useful for checking if it's within the 6000 character limit.
.. describe:: bool(b)
Returns whether the embed has any data set.
.. versionadded:: 2.0
Certain properties return an ``EmbedProxy``, a type Certain properties return an ``EmbedProxy``, a type
that acts similar to a regular :class:`dict` except using dotted access, that acts similar to a regular :class:`dict` except using dotted access,
e.g. ``embed.author.icon_url``. If the attribute e.g. ``embed.author.icon_url``. If the attribute
@ -91,7 +146,9 @@ class Embed:
The URL of the embed. The URL of the embed.
This can be set during initialisation. This can be set during initialisation.
timestamp: :class:`datetime.datetime` timestamp: :class:`datetime.datetime`
The timestamp of the embed content. This could be a naive or aware datetime. The timestamp of the embed content. This is an aware datetime.
If a naive datetime is passed, it is converted to an aware
datetime with the local timezone.
colour: Union[:class:`Colour`, :class:`int`] colour: Union[:class:`Colour`, :class:`int`]
The colour code of the embed. Aliased to ``color`` as well. The colour code of the embed. Aliased to ``color`` as well.
This can be set during initialisation. This can be set during initialisation.
@ -100,27 +157,47 @@ class Embed:
to denote that the value or attribute is empty. to denote that the value or attribute is empty.
""" """
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer', __slots__ = (
'_image', '_thumbnail', '_video', '_provider', '_author', 'title',
'_fields', 'description') 'url',
'type',
'_timestamp',
'_colour',
'_footer',
'_image',
'_thumbnail',
'_video',
'_provider',
'_author',
'_fields',
'description',
)
Empty = EmptyEmbed Empty: Final = EmptyEmbed
def __init__(self, **kwargs): def __init__(
# swap the colour/color aliases self,
try: *,
colour = kwargs['colour'] colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
except KeyError: color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
colour = kwargs.get('color', os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed)) title: MaybeEmpty[str] = EmptyEmbed,
type: EmbedType = 'rich',
url: MaybeEmpty[str] = EmptyEmbed,
description: MaybeEmpty[str] = EmptyEmbed,
timestamp: datetime.datetime = None,
):
if colour is EmptyEmbed and color is EmptyEmbed:
colour = os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed)
if isinstance(colour, str): if isinstance(colour, str):
colour = int(colour, 16) colour = int(colour, 16)
else:
colour = colour if colour is not EmptyEmbed else color
self.colour = colour self.colour = colour
self.title = kwargs.get('title', EmptyEmbed) self.title = title
self.type = kwargs.get('type', 'rich') self.type = type
self.url = kwargs.get('url', EmptyEmbed) self.url = url
self.description = kwargs.get('description', EmptyEmbed) self.description = description
if self.title is not EmptyEmbed: if self.title is not EmptyEmbed:
self.title = str(self.title) self.title = str(self.title)
@ -131,15 +208,13 @@ class Embed:
if self.url is not EmptyEmbed: if self.url is not EmptyEmbed:
self.url = str(self.url) self.url = str(self.url)
try: if timestamp:
timestamp = kwargs['timestamp'] if timestamp.tzinfo is None:
except KeyError: timestamp = timestamp.astimezone()
pass
else:
self.timestamp = timestamp self.timestamp = timestamp
@classmethod @classmethod
def from_dict(cls, data): def from_dict(cls: Type[E], data: EmbedData) -> E:
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the """Converts a :class:`dict` to a :class:`Embed` provided it is in the
format that Discord expects it to be in. format that Discord expects it to be in.
@ -155,7 +230,7 @@ class Embed:
The dictionary to convert into an embed. The dictionary to convert into an embed.
""" """
# we are bypassing __init__ here since it doesn't apply here # we are bypassing __init__ here since it doesn't apply here
self = cls.__new__(cls) self: E = cls.__new__(cls)
# fill in the basic fields # fill in the basic fields
@ -195,11 +270,11 @@ class Embed:
return self return self
def copy(self): def copy(self: E) -> E:
"""Returns a shallow copy of the embed.""" """Returns a shallow copy of the embed."""
return Embed.from_dict(self.to_dict()) return self.__class__.from_dict(self.to_dict())
def __len__(self): def __len__(self) -> int:
total = len(self.title) + len(self.description) total = len(self.title) + len(self.description)
for field in getattr(self, '_fields', []): for field in getattr(self, '_fields', []):
total += len(field['name']) + len(field['value']) total += len(field['name']) + len(field['value'])
@ -220,43 +295,61 @@ class Embed:
return total return total
def __bool__(self) -> bool:
return any(
(
self.title,
self.url,
self.description,
self.colour,
self.fields,
self.timestamp,
self.author,
self.thumbnail,
self.footer,
self.image,
self.provider,
self.video,
)
)
@property @property
def colour(self): def colour(self) -> MaybeEmpty[Colour]:
return getattr(self, '_colour', EmptyEmbed) return getattr(self, '_colour', EmptyEmbed)
@colour.setter @colour.setter
def colour(self, value): def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
if isinstance(value, (Colour, _EmptyEmbed)): if isinstance(value, (Colour, _EmptyEmbed)):
self._colour = value self._colour = value
elif isinstance(value, int): elif isinstance(value, int):
self._colour = Colour(value=value) self._colour = Colour(value=value)
else: else:
raise TypeError('Expected discord.Colour, int, or Embed.Empty but received %s instead.' % value.__class__.__name__) raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.')
color = colour color = colour
@property @property
def timestamp(self): def timestamp(self) -> MaybeEmpty[datetime.datetime]:
return getattr(self, '_timestamp', EmptyEmbed) return getattr(self, '_timestamp', EmptyEmbed)
@timestamp.setter @timestamp.setter
def timestamp(self, value): def timestamp(self, value: MaybeEmpty[datetime.datetime]):
if isinstance(value, (datetime.datetime, _EmptyEmbed)): if isinstance(value, (datetime.datetime, _EmptyEmbed)):
self._timestamp = value self._timestamp = value
else: else:
raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__) raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
@property @property
def footer(self): def footer(self) -> _EmbedFooterProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the footer contents. """Returns an ``EmbedProxy`` denoting the footer contents.
See :meth:`set_footer` for possible values you can access. See :meth:`set_footer` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_footer', {})) return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed): def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
"""Sets the footer for the embed content. """Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -280,8 +373,8 @@ class Embed:
return self return self
@property @property
def image(self): def image(self) -> _EmbedMediaProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the image contents. """Returns an ``EmbedProxy`` denoting the image contents.
Possible attributes you can access are: Possible attributes you can access are:
@ -292,9 +385,9 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_image', {})) return EmbedProxy(getattr(self, '_image', {})) # type: ignore
def set_image(self, *, url): def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
"""Sets the image for the embed content. """Sets the image for the embed content.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -316,14 +409,14 @@ class Embed:
pass pass
else: else:
self._image = { self._image = {
'url': str(url) 'url': str(url),
} }
return self return self
@property @property
def thumbnail(self): def thumbnail(self) -> _EmbedMediaProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the thumbnail contents. """Returns an ``EmbedProxy`` denoting the thumbnail contents.
Possible attributes you can access are: Possible attributes you can access are:
@ -334,9 +427,9 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_thumbnail', {})) return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
def set_thumbnail(self, *, url): def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
"""Sets the thumbnail for the embed content. """Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -358,14 +451,14 @@ class Embed:
pass pass
else: else:
self._thumbnail = { self._thumbnail = {
'url': str(url) 'url': str(url),
} }
return self return self
@property @property
def video(self): def video(self) -> _EmbedVideoProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the video contents. """Returns an ``EmbedProxy`` denoting the video contents.
Possible attributes include: Possible attributes include:
@ -375,29 +468,29 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_video', {})) return EmbedProxy(getattr(self, '_video', {})) # type: ignore
@property @property
def provider(self): def provider(self) -> _EmbedProviderProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the provider contents. """Returns an ``EmbedProxy`` denoting the provider contents.
The only attributes that might be accessed are ``name`` and ``url``. The only attributes that might be accessed are ``name`` and ``url``.
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_provider', {})) return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
@property @property
def author(self): def author(self) -> _EmbedAuthorProxy:
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the author contents. """Returns an ``EmbedProxy`` denoting the author contents.
See :meth:`set_author` for possible values you can access. See :meth:`set_author` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return EmbedProxy(getattr(self, '_author', {})) return EmbedProxy(getattr(self, '_author', {})) # type: ignore
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed): def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
"""Sets the author for the embed content. """Sets the author for the embed content.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -414,7 +507,7 @@ class Embed:
""" """
self._author = { self._author = {
'name': str(name) 'name': str(name),
} }
if url is not EmptyEmbed: if url is not EmptyEmbed:
@ -425,7 +518,7 @@ class Embed:
return self return self
def remove_author(self): def remove_author(self: E) -> E:
"""Clears embed's author information. """Clears embed's author information.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -441,16 +534,21 @@ class Embed:
return self return self
@property @property
<<<<<<< HEAD
def fields(self): def fields(self):
"""List[Union[``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.
=======
def fields(self) -> List[_EmbedFieldProxy]:
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
See :meth:`add_field` for possible values you can access. See :meth:`add_field` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned. If the attribute has no value then :attr:`Empty` is returned.
""" """
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
def add_field(self, *, name, value, inline=True): def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
"""Adds a field to the embed object. """Adds a field to the embed object.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -469,7 +567,7 @@ class Embed:
field = { field = {
'inline': inline, 'inline': inline,
'name': str(name), 'name': str(name),
'value': str(value) 'value': str(value),
} }
try: try:
@ -479,7 +577,7 @@ class Embed:
return self return self
def insert_field_at(self, index, *, name, value, inline=True): def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
"""Inserts a field before a specified index to the embed. """Inserts a field before a specified index to the embed.
This function returns the class instance to allow for fluent-style This function returns the class instance to allow for fluent-style
@ -502,7 +600,7 @@ class Embed:
field = { field = {
'inline': inline, 'inline': inline,
'name': str(name), 'name': str(name),
'value': str(value) 'value': str(value),
} }
try: try:
@ -512,14 +610,14 @@ class Embed:
return self return self
def clear_fields(self): def clear_fields(self) -> None:
"""Removes all fields from this embed.""" """Removes all fields from this embed."""
try: try:
self._fields.clear() self._fields.clear()
except AttributeError: except AttributeError:
self._fields = [] self._fields = []
def remove_field(self, index): def remove_field(self, index: int) -> None:
"""Removes a field at a specified index. """Removes a field at a specified index.
If the index is invalid or out of bounds then the error is If the index is invalid or out of bounds then the error is
@ -540,7 +638,7 @@ class Embed:
except (AttributeError, IndexError): except (AttributeError, IndexError):
pass pass
def set_field_at(self, index, *, name, value, inline=True): def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
"""Modifies a field to the embed object. """Modifies a field to the embed object.
The index must point to a valid pre-existing field. The index must point to a valid pre-existing field.
@ -575,15 +673,17 @@ class Embed:
field['inline'] = inline field['inline'] = inline
return self return self
def to_dict(self): def to_dict(self) -> EmbedData:
"""Converts this embed object into a dict.""" """Converts this embed object into a dict."""
# add in the raw data into the dict # add in the raw data into the dict
# fmt: off
result = { result = {
key[1:]: getattr(self, key) key[1:]: getattr(self, key)
for key in self.__slots__ for key in self.__slots__
if key[0] == '_' and hasattr(self, key) if key[0] == '_' and hasattr(self, key)
} }
# fmt: on
# deal with basic convenience wrappers # deal with basic convenience wrappers
@ -619,4 +719,4 @@ class Embed:
if self.title: if self.title:
result['title'] = self.title result['title'] = self.title
return result return result # type: ignore

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -29,6 +27,10 @@ from . import utils
from .partial_emoji import _EmojiTag from .partial_emoji import _EmojiTag
from .user import User from .user import User
__all__ = (
'Emoji',
)
class Emoji(_EmojiTag): class Emoji(_EmojiTag):
"""Represents a custom emoji. """Represents a custom emoji.

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -26,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
import types import types
from collections import namedtuple from collections import namedtuple
from typing import Any, TYPE_CHECKING, Type, TypeVar
__all__ = ( __all__ = (
'Enum', 'Enum',
@ -37,18 +36,12 @@ __all__ = (
'ContentFilter', 'ContentFilter',
'Status', 'Status',
'DefaultAvatar', 'DefaultAvatar',
'RelationshipType',
'AuditLogAction', 'AuditLogAction',
'AuditLogActionCategory', 'AuditLogActionCategory',
'UserFlags', 'UserFlags',
'ActivityType', 'ActivityType',
'HypeSquadHouse',
'NotificationLevel', 'NotificationLevel',
'PremiumType',
'UserContentFilter',
'FriendFlags',
'TeamMembershipState', 'TeamMembershipState',
'Theme',
'WebhookType', 'WebhookType',
'ExpireBehaviour', 'ExpireBehaviour',
'ExpireBehavior', 'ExpireBehavior',
@ -57,8 +50,8 @@ __all__ = (
def _create_value_cls(name): def _create_value_cls(name):
cls = namedtuple('_EnumValue_' + name, 'name value') cls = namedtuple('_EnumValue_' + name, 'name value')
cls.__repr__ = lambda self: '<%s.%s: %r>' % (name, self.name, self.value) cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
cls.__str__ = lambda self: '%s.%s' % (name, self.name) cls.__str__ = lambda self: f'{name}.{self.name}'
return cls return cls
def _is_descriptor(obj): def _is_descriptor(obj):
@ -98,6 +91,7 @@ class EnumMeta(type):
attrs['_enum_value_map_'] = value_mapping attrs['_enum_value_map_'] = value_mapping
attrs['_enum_member_map_'] = member_mapping attrs['_enum_member_map_'] = member_mapping
attrs['_enum_member_names_'] = member_names attrs['_enum_member_names_'] = member_names
attrs['_enum_value_cls_'] = value_cls
actual_cls = super().__new__(cls, name, bases, attrs) actual_cls = super().__new__(cls, name, bases, attrs)
value_cls._actual_enum_cls_ = actual_cls value_cls._actual_enum_cls_ = actual_cls
return actual_cls return actual_cls
@ -112,7 +106,7 @@ class EnumMeta(type):
return len(cls._enum_member_names_) return len(cls._enum_member_names_)
def __repr__(cls): def __repr__(cls):
return '<enum %r>' % cls.__name__ return f'<enum {cls.__name__}>'
@property @property
def __members__(cls): def __members__(cls):
@ -122,7 +116,7 @@ class EnumMeta(type):
try: try:
return cls._enum_value_map_[value] return cls._enum_value_map_[value]
except (KeyError, TypeError): except (KeyError, TypeError):
raise ValueError("%r is not a valid %s" % (value, cls.__name__)) raise ValueError(f"{value!r} is not a valid {cls.__name__}")
def __getitem__(cls, key): def __getitem__(cls, key):
return cls._enum_member_map_[key] return cls._enum_member_map_[key]
@ -141,14 +135,16 @@ class EnumMeta(type):
except AttributeError: except AttributeError:
return False return False
class Enum(metaclass=EnumMeta): if TYPE_CHECKING:
@classmethod from enum import Enum
def try_value(cls, value): else:
try: class Enum(metaclass=EnumMeta):
return cls._enum_value_map_[value] @classmethod
except (KeyError, TypeError): def try_value(cls, value):
return value try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
return value
class ChannelType(Enum): class ChannelType(Enum):
text = 0 text = 0
@ -244,22 +240,6 @@ class ContentFilter(Enum):
def __str__(self): def __str__(self):
return self.name return self.name
class UserContentFilter(Enum):
disabled = 0
friends = 1
all_messages = 2
class FriendFlags(Enum):
noone = 0
mutual_guilds = 1
mutual_friends = 2
guild_and_friends = 3
everyone = 4
class Theme(Enum):
light = 'light'
dark = 'dark'
class Status(Enum): class Status(Enum):
online = 'online' online = 'online'
offline = 'offline' offline = 'offline'
@ -282,12 +262,6 @@ class DefaultAvatar(Enum):
def __str__(self): def __str__(self):
return self.name return self.name
class RelationshipType(Enum):
friend = 1
blocked = 2
incoming_request = 3
outgoing_request = 4
class NotificationLevel(Enum): class NotificationLevel(Enum):
all_messages = 0 all_messages = 0
only_mentions = 1 only_mentions = 1
@ -429,15 +403,6 @@ class ActivityType(Enum):
def __int__(self): def __int__(self):
return self.value return self.value
class HypeSquadHouse(Enum):
bravery = 1
brilliance = 2
balance = 3
class PremiumType(Enum):
nitro_classic = 1
nitro = 2
class TeamMembershipState(Enum): class TeamMembershipState(Enum):
invited = 1 invited = 1
accepted = 2 accepted = 2
@ -457,13 +422,24 @@ class StickerType(Enum):
apng = 2 apng = 2
lottie = 3 lottie = 3
def try_enum(cls, val): class InteractionType(Enum):
ping = 1
application_command = 2
T = TypeVar('T')
def create_unknown_value(cls: Type[T], val: Any) -> T:
value_cls = cls._enum_value_cls_ # type: ignore
name = f'unknown_{val}'
return value_cls(name=name, value=val)
def try_enum(cls: Type[T], val: Any) -> T:
"""A function that tries to turn the value into enum ``cls``. """A function that tries to turn the value into enum ``cls``.
If it fails it returns the value instead. If it fails it returns a proxy invalid value instead.
""" """
try: try:
return cls._enum_value_map_[val] return cls._enum_value_map_[val] # type: ignore
except (KeyError, TypeError, AttributeError): except (KeyError, TypeError, AttributeError):
return val return create_unknown_value(cls, val)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,6 +22,22 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
__all__ = (
'DiscordException',
'ClientException',
'NoMoreItems',
'GatewayNotFound',
'HTTPException',
'Forbidden',
'NotFound',
'DiscordServerError',
'InvalidData',
'InvalidArgument',
'LoginFailure',
'ConnectionClosed',
'PrivilegedIntentsRequired',
)
class DiscordException(Exception): class DiscordException(Exception):
"""Base exception class for discord.py """Base exception class for discord.py
@ -48,7 +62,7 @@ class GatewayNotFound(DiscordException):
for the :class:`Client` websocket is not found.""" for the :class:`Client` websocket is not found."""
def __init__(self): def __init__(self):
message = 'The gateway to connect to discord was not found.' message = 'The gateway to connect to discord was not found.'
super(GatewayNotFound, self).__init__(message) super().__init__(message)
def flatten_error_dict(d, key=''): def flatten_error_dict(d, key=''):
items = [] items = []
@ -174,7 +188,7 @@ class ConnectionClosed(ClientException):
# aiohttp doesn't seem to consistently provide close reason # aiohttp doesn't seem to consistently provide close reason
self.reason = '' self.reason = ''
self.shard_id = shard_id self.shard_id = shard_id
super().__init__('Shard ID %s WebSocket closed with %s' % (self.shard_id, self.code)) super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}')
class PrivilegedIntentsRequired(ClientException): class PrivilegedIntentsRequired(ClientException):
"""Exception that's thrown when the gateway is requesting privileged intents """Exception that's thrown when the gateway is requesting privileged intents

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
discord.ext.commands discord.ext.commands
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@ -10,8 +8,8 @@ An extension module to facilitate creation of bot commands.
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or from .bot import *
from .context import Context from .context import *
from .core import * from .core import *
from .errors import * from .errors import *
from .help import * from .help import *

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -42,12 +40,19 @@ from . import errors
from .help import HelpCommand, DefaultHelpCommand from .help import HelpCommand, DefaultHelpCommand
from .cog import Cog from .cog import Cog
__all__ = (
'when_mentioned',
'when_mentioned_or',
'Bot',
'AutoShardedBot',
)
def when_mentioned(bot, msg): def when_mentioned(bot, msg):
"""A callable that implements a command prefix equivalent to being mentioned. """A callable that implements a command prefix equivalent to being mentioned.
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
""" """
return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id] return [f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
def when_mentioned_or(*prefixes): def when_mentioned_or(*prefixes):
"""A callable that implements when mentioned or other prefixes provided. """A callable that implements when mentioned or other prefixes provided.
@ -116,7 +121,7 @@ class BotBase(GroupMixin):
raise TypeError('Both owner_id and owner_ids are set.') raise TypeError('Both owner_id and owner_ids are set.')
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection): if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
raise TypeError('owner_ids must be a collection not {0.__class__!r}'.format(self.owner_ids)) raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
if options.pop('self_bot', False): if options.pop('self_bot', False):
self._skip_check = lambda x, y: x != y self._skip_check = lambda x, y: x != y
@ -194,7 +199,7 @@ class BotBase(GroupMixin):
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None: if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
return return
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr) print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
# global check registration # global check registration
@ -986,7 +991,7 @@ class BotBase(GroupMixin):
else: else:
self.dispatch('command_completion', ctx) self.dispatch('command_completion', ctx)
elif ctx.invoked_with: elif ctx.invoked_with:
exc = errors.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found')
self.dispatch('command_error', ctx, exc) self.dispatch('command_error', ctx, exc)
async def process_commands(self, message): async def process_commands(self, message):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -130,7 +128,7 @@ class CogMeta(type):
value = value.__func__ value = value.__func__
if isinstance(value, _BaseCommand): if isinstance(value, _BaseCommand):
if is_static_method: if is_static_method:
raise TypeError('Command in method {0}.{1!r} must not be staticmethod.'.format(base, elem)) raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
if elem.startswith(('cog_', 'bot_')): if elem.startswith(('cog_', 'bot_')):
raise TypeError(no_bot_cog.format(base, elem)) raise TypeError(no_bot_cog.format(base, elem))
commands[elem] = value commands[elem] = value
@ -285,7 +283,7 @@ class Cog(metaclass=CogMeta):
""" """
if name is not None and not isinstance(name, str): if name is not None and not isinstance(name, str):
raise TypeError('Cog.listener expected str but received {0.__class__.__name__!r} instead.'.format(name)) raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
def decorator(func): def decorator(func):
actual = func actual = func

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -28,6 +26,10 @@ import re
import discord.abc import discord.abc
import discord.utils import discord.utils
__all__ = (
'Context',
)
class Context(discord.abc.Messageable): class Context(discord.abc.Messageable):
r"""Represents the context in which a command is being invoked under. r"""Represents the context in which a command is being invoked under.
@ -103,7 +105,7 @@ class Context(discord.abc.Messageable):
pattern = re.compile(r"<@!?%s>" % user.id) pattern = re.compile(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix) return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
async def invoke(self, *args, **kwargs): async def invoke(self, command, /, *args, **kwargs):
r"""|coro| r"""|coro|
Calls a command with the arguments given. Calls a command with the arguments given.
@ -120,10 +122,6 @@ class Context(discord.abc.Messageable):
You must take care in passing the proper arguments when You must take care in passing the proper arguments when
using this function. using this function.
.. warning::
The first parameter passed **must** be the command being invoked.
Parameters Parameters
----------- -----------
command: :class:`.Command` command: :class:`.Command`
@ -138,18 +136,12 @@ class Context(discord.abc.Messageable):
TypeError TypeError
The command argument to invoke is missing. The command argument to invoke is missing.
""" """
try:
command = args[0]
except IndexError:
raise TypeError('Missing command to invoke.') from None
arguments = [] arguments = []
if command.cog is not None: if command.cog is not None:
arguments.append(command.cog) arguments.append(command.cog)
arguments.append(self) arguments.append(self)
arguments.extend(args[1:]) arguments.extend(args)
ret = await command.callback(*arguments, **kwargs) ret = await command.callback(*arguments, **kwargs)
return ret return ret

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,14 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations
import re import re
import inspect import inspect
import typing from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
import discord import discord
from .errors import * from .errors import *
if TYPE_CHECKING:
from .context import Context
__all__ = ( __all__ = (
'Converter', 'Converter',
'MemberConverter', 'MemberConverter',
@ -56,6 +59,7 @@ __all__ = (
'Greedy', 'Greedy',
) )
def _get_from_guilds(bot, getter, argument): def _get_from_guilds(bot, getter, argument):
result = None result = None
for guild in bot.guilds: for guild in bot.guilds:
@ -64,9 +68,13 @@ def _get_from_guilds(bot, getter, argument):
return result return result
return result return result
_utils_get = discord.utils.get
class Converter: _utils_get = discord.utils.get
T = TypeVar('T', covariant=True)
@runtime_checkable
class Converter(Protocol[T]):
"""The base class of custom converters that require the :class:`.Context` """The base class of custom converters that require the :class:`.Context`
to be passed to be useful. to be passed to be useful.
@ -77,7 +85,7 @@ class Converter:
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`. method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> T:
"""|coro| """|coro|
The method to override to do conversion logic. The method to override to do conversion logic.
@ -102,7 +110,7 @@ class Converter:
""" """
raise NotImplementedError('Derived classes need to implement this.') raise NotImplementedError('Derived classes need to implement this.')
class IDConverter(Converter): class IDConverter(Converter[T]):
def __init__(self): def __init__(self):
self._id_regex = re.compile(r'([0-9]{15,20})$') self._id_regex = re.compile(r'([0-9]{15,20})$')
super().__init__() super().__init__()
@ -110,7 +118,7 @@ class IDConverter(Converter):
def _get_id_match(self, argument): def _get_id_match(self, argument):
return self._id_regex.match(argument) return self._id_regex.match(argument)
class MemberConverter(IDConverter): class MemberConverter(IDConverter[discord.Member]):
"""Converts to a :class:`~discord.Member`. """Converts to a :class:`~discord.Member`.
All lookups are via the local guild. If in a DM context, then the lookup All lookups are via the local guild. If in a DM context, then the lookup
@ -163,7 +171,7 @@ class MemberConverter(IDConverter):
return None return None
return members[0] return members[0]
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Member:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
guild = ctx.guild guild = ctx.guild
@ -196,7 +204,7 @@ class MemberConverter(IDConverter):
return result return result
class UserConverter(IDConverter): class UserConverter(IDConverter[discord.User]):
"""Converts to a :class:`~discord.User`. """Converts to a :class:`~discord.User`.
All lookups are via the global user cache. All lookups are via the global user cache.
@ -215,7 +223,7 @@ class UserConverter(IDConverter):
This converter now lazily fetches users from the HTTP APIs if an ID is passed This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache. and it's not available in cache.
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.User:
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
result = None result = None
state = ctx._state state = ctx._state
@ -255,7 +263,7 @@ class UserConverter(IDConverter):
return result return result
class PartialMessageConverter(Converter): class PartialMessageConverter(Converter[discord.PartialMessage]):
"""Converts to a :class:`discord.PartialMessage`. """Converts to a :class:`discord.PartialMessage`.
.. versionadded:: 1.7 .. versionadded:: 1.7
@ -266,7 +274,8 @@ class PartialMessageConverter(Converter):
2. By message ID (The message is assumed to be in the context channel.) 2. By message ID (The message is assumed to be in the context channel.)
3. By message URL 3. By message URL
""" """
def _get_id_matches(self, argument): @staticmethod
def _get_id_matches(argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$') id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
link_regex = re.compile( link_regex = re.compile(
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/' r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
@ -279,14 +288,14 @@ class PartialMessageConverter(Converter):
channel_id = match.group("channel_id") channel_id = match.group("channel_id")
return int(match.group("message_id")), int(channel_id) if channel_id else None return int(match.group("message_id")), int(channel_id) if channel_id else None
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
message_id, channel_id = self._get_id_matches(argument) message_id, channel_id = self._get_id_matches(argument)
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
if not channel: if not channel:
raise ChannelNotFound(channel_id) raise ChannelNotFound(channel_id)
return discord.PartialMessage(channel=channel, id=message_id) return discord.PartialMessage(channel=channel, id=message_id)
class MessageConverter(PartialMessageConverter): class MessageConverter(IDConverter[discord.Message]):
"""Converts to a :class:`discord.Message`. """Converts to a :class:`discord.Message`.
.. versionadded:: 1.1 .. versionadded:: 1.1
@ -300,8 +309,8 @@ class MessageConverter(PartialMessageConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument` Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Message:
message_id, channel_id = self._get_id_matches(argument) message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
message = ctx.bot._connection._get_message(message_id) message = ctx.bot._connection._get_message(message_id)
if message: if message:
return message return message
@ -315,7 +324,7 @@ class MessageConverter(PartialMessageConverter):
except discord.Forbidden: except discord.Forbidden:
raise ChannelNotReadable(channel) raise ChannelNotReadable(channel)
class TextChannelConverter(IDConverter): class TextChannelConverter(IDConverter[discord.TextChannel]):
"""Converts to a :class:`~discord.TextChannel`. """Converts to a :class:`~discord.TextChannel`.
All lookups are via the local guild. If in a DM context, then the lookup All lookups are via the local guild. If in a DM context, then the lookup
@ -330,7 +339,7 @@ class TextChannelConverter(IDConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
@ -357,7 +366,7 @@ class TextChannelConverter(IDConverter):
return result return result
class VoiceChannelConverter(IDConverter): class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
"""Converts to a :class:`~discord.VoiceChannel`. """Converts to a :class:`~discord.VoiceChannel`.
All lookups are via the local guild. If in a DM context, then the lookup All lookups are via the local guild. If in a DM context, then the lookup
@ -372,7 +381,7 @@ class VoiceChannelConverter(IDConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None result = None
@ -398,7 +407,7 @@ class VoiceChannelConverter(IDConverter):
return result return result
class StageChannelConverter(IDConverter): class StageChannelConverter(IDConverter[discord.StageChannel]):
"""Converts to a :class:`~discord.StageChannel`. """Converts to a :class:`~discord.StageChannel`.
.. versionadded:: 1.7 .. versionadded:: 1.7
@ -412,7 +421,7 @@ class StageChannelConverter(IDConverter):
2. Lookup by mention. 2. Lookup by mention.
3. Lookup by name 3. Lookup by name
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None result = None
@ -438,7 +447,7 @@ class StageChannelConverter(IDConverter):
return result return result
class CategoryChannelConverter(IDConverter): class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
"""Converts to a :class:`~discord.CategoryChannel`. """Converts to a :class:`~discord.CategoryChannel`.
All lookups are via the local guild. If in a DM context, then the lookup All lookups are via the local guild. If in a DM context, then the lookup
@ -453,7 +462,7 @@ class CategoryChannelConverter(IDConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
@ -480,7 +489,7 @@ class CategoryChannelConverter(IDConverter):
return result return result
class StoreChannelConverter(IDConverter): class StoreChannelConverter(IDConverter[discord.StoreChannel]):
"""Converts to a :class:`~discord.StoreChannel`. """Converts to a :class:`~discord.StoreChannel`.
All lookups are via the local guild. If in a DM context, then the lookup All lookups are via the local guild. If in a DM context, then the lookup
@ -495,7 +504,7 @@ class StoreChannelConverter(IDConverter):
.. versionadded:: 1.7 .. versionadded:: 1.7
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
bot = ctx.bot bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None result = None
@ -521,7 +530,7 @@ class StoreChannelConverter(IDConverter):
return result return result
class ColourConverter(Converter): class ColourConverter(Converter[discord.Colour]):
"""Converts to a :class:`~discord.Colour`. """Converts to a :class:`~discord.Colour`.
.. versionchanged:: 1.5 .. versionchanged:: 1.5
@ -582,7 +591,7 @@ class ColourConverter(Converter):
blue = self.parse_rgb_number(argument, match.group('b')) blue = self.parse_rgb_number(argument, match.group('b'))
return discord.Color.from_rgb(red, green, blue) return discord.Color.from_rgb(red, green, blue)
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Colour:
if argument[0] == '#': if argument[0] == '#':
return self.parse_hex_number(argument[1:]) return self.parse_hex_number(argument[1:])
@ -605,7 +614,7 @@ class ColourConverter(Converter):
ColorConverter = ColourConverter ColorConverter = ColourConverter
class RoleConverter(IDConverter): class RoleConverter(IDConverter[discord.Role]):
"""Converts to a :class:`~discord.Role`. """Converts to a :class:`~discord.Role`.
All lookups are via the local guild. If in a DM context, the converter raises All lookups are via the local guild. If in a DM context, the converter raises
@ -620,7 +629,7 @@ class RoleConverter(IDConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument` Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Role:
guild = ctx.guild guild = ctx.guild
if not guild: if not guild:
raise NoPrivateMessage() raise NoPrivateMessage()
@ -635,12 +644,12 @@ class RoleConverter(IDConverter):
raise RoleNotFound(argument) raise RoleNotFound(argument)
return result return result
class GameConverter(Converter): class GameConverter(Converter[discord.Game]):
"""Converts to :class:`~discord.Game`.""" """Converts to :class:`~discord.Game`."""
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Game:
return discord.Game(name=argument) return discord.Game(name=argument)
class InviteConverter(Converter): class InviteConverter(Converter[discord.Invite]):
"""Converts to a :class:`~discord.Invite`. """Converts to a :class:`~discord.Invite`.
This is done via an HTTP request using :meth:`.Bot.fetch_invite`. This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
@ -648,14 +657,14 @@ class InviteConverter(Converter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument` Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Invite:
try: try:
invite = await ctx.bot.fetch_invite(argument) invite = await ctx.bot.fetch_invite(argument)
return invite return invite
except Exception as exc: except Exception as exc:
raise BadInviteArgument() from exc raise BadInviteArgument() from exc
class GuildConverter(IDConverter): class GuildConverter(IDConverter[discord.Guild]):
"""Converts to a :class:`~discord.Guild`. """Converts to a :class:`~discord.Guild`.
The lookup strategy is as follows (in order): The lookup strategy is as follows (in order):
@ -666,7 +675,7 @@ class GuildConverter(IDConverter):
.. versionadded:: 1.7 .. versionadded:: 1.7
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Guild:
match = self._get_id_match(argument) match = self._get_id_match(argument)
result = None result = None
@ -681,7 +690,7 @@ class GuildConverter(IDConverter):
raise GuildNotFound(argument) raise GuildNotFound(argument)
return result return result
class EmojiConverter(IDConverter): class EmojiConverter(IDConverter[discord.Emoji]):
"""Converts to a :class:`~discord.Emoji`. """Converts to a :class:`~discord.Emoji`.
All lookups are done for the local guild first, if available. If that lookup All lookups are done for the local guild first, if available. If that lookup
@ -696,7 +705,7 @@ class EmojiConverter(IDConverter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument` Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument) match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
result = None result = None
bot = ctx.bot bot = ctx.bot
@ -724,7 +733,7 @@ class EmojiConverter(IDConverter):
return result return result
class PartialEmojiConverter(Converter): class PartialEmojiConverter(Converter[discord.PartialEmoji]):
"""Converts to a :class:`~discord.PartialEmoji`. """Converts to a :class:`~discord.PartialEmoji`.
This is done by extracting the animated flag, name and ID from the emoji. This is done by extracting the animated flag, name and ID from the emoji.
@ -732,7 +741,7 @@ class PartialEmojiConverter(Converter):
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument` Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument) match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
if match: if match:
@ -745,7 +754,7 @@ class PartialEmojiConverter(Converter):
raise PartialEmojiConversionFailure(argument) raise PartialEmojiConversionFailure(argument)
class clean_content(Converter): class clean_content(Converter[str]):
"""Converts the argument to mention scrubbed version of """Converts the argument to mention scrubbed version of
said content. said content.
@ -770,14 +779,14 @@ class clean_content(Converter):
self.escape_markdown = escape_markdown self.escape_markdown = escape_markdown
self.remove_markdown = remove_markdown self.remove_markdown = remove_markdown
async def convert(self, ctx, argument): async def convert(self, ctx: Context, argument: str) -> str:
message = ctx.message message = ctx.message
transformations = {} transformations = {}
if self.fix_channel_mentions and ctx.guild: if self.fix_channel_mentions and ctx.guild:
def resolve_channel(id, *, _get=ctx.guild.get_channel): def resolve_channel(id, *, _get=ctx.guild.get_channel):
ch = _get(id) ch = _get(id)
return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel') return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions) transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
@ -792,12 +801,12 @@ class clean_content(Converter):
transformations.update( transformations.update(
('<@%s>' % member_id, resolve_member(member_id)) (f'<@{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions for member_id in message.raw_mentions
) )
transformations.update( transformations.update(
('<@!%s>' % member_id, resolve_member(member_id)) (f'<@!{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions for member_id in message.raw_mentions
) )
@ -807,7 +816,7 @@ class clean_content(Converter):
return '@' + r.name if r else '@deleted-role' return '@' + r.name if r else '@deleted-role'
transformations.update( transformations.update(
('<@&%s>' % role_id, resolve_role(role_id)) (f'<@&{role_id}>', resolve_role(role_id))
for role_id in message.raw_role_mentions for role_id in message.raw_role_mentions
) )
@ -842,10 +851,10 @@ class _Greedy:
raise TypeError('Greedy[...] expects a type or a Converter instance.') raise TypeError('Greedy[...] expects a type or a Converter instance.')
if converter is str or converter is type(None) or converter is _Greedy: if converter is str or converter is type(None) or converter is _Greedy:
raise TypeError('Greedy[%s] is invalid.' % converter.__name__) raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
if getattr(converter, '__origin__', None) is typing.Union and type(None) in converter.__args__: if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
raise TypeError('Greedy[%r] is invalid.' % converter) raise TypeError(f'Greedy[{converter!r}] is invalid.')
return self.__class__(converter=converter) return self.__class__(converter=converter)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -255,7 +253,7 @@ class MaxConcurrency:
raise ValueError('max_concurrency \'number\' cannot be less than 1') raise ValueError('max_concurrency \'number\' cannot be less than 1')
if not isinstance(per, BucketType): if not isinstance(per, BucketType):
raise TypeError('max_concurrency \'per\' must be of type BucketType not %r' % type(per)) raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
def copy(self): def copy(self):
return self.__class__(self.number, per=self.per, wait=self.wait) return self.__class__(self.number, per=self.per, wait=self.wait)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -29,6 +27,7 @@ import functools
import inspect import inspect
import typing import typing
import datetime import datetime
import sys
import discord import discord
@ -301,17 +300,37 @@ class Command(_BaseCommand):
signature = inspect.signature(function) signature = inspect.signature(function)
self.params = signature.parameters.copy() self.params = signature.parameters.copy()
# PEP-563 allows postponing evaluation of annotations with a __future__ # see: https://bugs.python.org/issue41341
# import. When postponed, Parameter.annotation will be a string and must resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
# be replaced with the real value for the converters to work later on
try:
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
except NameError as e:
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
for key, value in self.params.items(): for key, value in self.params.items():
if isinstance(value.annotation, str): # coalesce the forward references
self.params[key] = value = value.replace(annotation=eval(value.annotation, function.__globals__)) if key in type_hints:
self.params[key] = value = value.replace(annotation=type_hints[key])
# fail early for when someone passes an unparameterized Greedy type # fail early for when someone passes an unparameterized Greedy type
if value.annotation is converters.Greedy: if value.annotation is converters.Greedy:
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.') raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
def _return_resolved(self, type, **kwargs):
return type
def _recursive_resolve(self, type, *, globals=None):
if not isinstance(type, typing.ForwardRef):
return type
resolved = eval(type.__forward_arg__, globals)
args = typing.get_args(resolved)
for index, arg in enumerate(args):
inner_resolve_result = self._recursive_resolve(arg, globals=globals)
resolved[index] = inner_resolve_result
return resolved
def add_check(self, func): def add_check(self, func):
"""Adds a check to the command. """Adds a check to the command.
@ -445,19 +464,13 @@ class Command(_BaseCommand):
converter = getattr(converters, converter.__name__ + 'Converter', converter) converter = getattr(converters, converter.__name__ + 'Converter', converter)
try: try:
if inspect.isclass(converter): if inspect.isclass(converter) and issubclass(converter, converters.Converter):
if issubclass(converter, converters.Converter): if inspect.ismethod(converter.convert):
instance = converter() return await converter.convert(ctx, argument)
ret = await instance.convert(ctx, argument)
return ret
else: else:
method = getattr(converter, 'convert', None) return await converter().convert(ctx, argument)
if method is not None and inspect.ismethod(method):
ret = await method(ctx, argument)
return ret
elif isinstance(converter, converters.Converter): elif isinstance(converter, converters.Converter):
ret = await converter.convert(ctx, argument) return await converter.convert(ctx, argument)
return ret
except CommandError: except CommandError:
raise raise
except Exception as exc: except Exception as exc:
@ -473,7 +486,7 @@ class Command(_BaseCommand):
except AttributeError: except AttributeError:
name = converter.__class__.__name__ name = converter.__class__.__name__
raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from exc raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
async def do_conversion(self, ctx, converter, argument, param): async def do_conversion(self, ctx, converter, argument, param):
try: try:
@ -775,7 +788,7 @@ class Command(_BaseCommand):
ctx.command = self ctx.command = self
if not await self.can_run(ctx): if not await self.can_run(ctx):
raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self)) raise CheckFailure(f'The check functions for command {self.qualified_name} failed.')
if self._max_concurrency is not None: if self._max_concurrency is not None:
await self._max_concurrency.acquire(ctx) await self._max_concurrency.acquire(ctx)
@ -1014,23 +1027,23 @@ class Command(_BaseCommand):
# do [name] since [name=None] or [name=] are not exactly useful for the user. # do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print: if should_print:
result.append('[%s=%s]' % (name, param.default) if not greedy else result.append(f'[{name}={param.default}]' if not greedy else
'[%s=%s]...' % (name, param.default)) f'[{name}={param.default}]...')
continue continue
else: else:
result.append('[%s]' % name) result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL: elif param.kind == param.VAR_POSITIONAL:
if self.require_var_positional: if self.require_var_positional:
result.append('<%s...>' % name) result.append(f'<{name}...>')
else: else:
result.append('[%s...]' % name) result.append(f'[{name}...]')
elif greedy: elif greedy:
result.append('[%s]...' % name) result.append(f'[{name}]...')
elif self._is_typing_optional(param.annotation): elif self._is_typing_optional(param.annotation):
result.append('[%s]' % name) result.append(f'[{name}]')
else: else:
result.append('<%s>' % name) result.append(f'<{name}>')
return ' '.join(result) return ' '.join(result)
@ -1062,14 +1075,14 @@ class Command(_BaseCommand):
""" """
if not self.enabled: if not self.enabled:
raise DisabledCommand('{0.name} command is disabled'.format(self)) raise DisabledCommand(f'{self.name} command is disabled')
original = ctx.command original = ctx.command
ctx.command = self ctx.command = self
try: try:
if not await ctx.bot.can_run(ctx): if not await ctx.bot.can_run(ctx):
raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self)) raise CheckFailure(f'The global check functions for command {self.qualified_name} failed.')
cog = self.cog cog = self.cog
if cog is not None: if cog is not None:
@ -1588,7 +1601,7 @@ def check_any(*checks):
try: try:
pred = wrapped.predicate pred = wrapped.predicate
except AttributeError: except AttributeError:
raise TypeError('%r must be wrapped by commands.check decorator' % wrapped) from None raise TypeError(f'{wrapped!r} must be wrapped by commands.check decorator') from None
else: else:
unwrapped.append(pred) unwrapped.append(pred)
@ -1776,7 +1789,7 @@ def has_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid: if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid))) raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx): def predicate(ctx):
ch = ctx.channel ch = ctx.channel
@ -1801,7 +1814,7 @@ def bot_has_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid: if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid))) raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx): def predicate(ctx):
guild = ctx.guild guild = ctx.guild
@ -1829,7 +1842,7 @@ def has_guild_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid: if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid))) raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx): def predicate(ctx):
if not ctx.guild: if not ctx.guild:
@ -1854,7 +1867,7 @@ def bot_has_guild_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid: if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid))) raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx): def predicate(ctx):
if not ctx.guild: if not ctx.guild:
@ -1961,7 +1974,7 @@ def cooldown(rate, per, type=BucketType.default):
The amount of seconds to wait for a cooldown when it's been triggered. The amount of seconds to wait for a cooldown when it's been triggered.
type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]] type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]]
The type of cooldown to have. If callable, should return a key for the mapping. The type of cooldown to have. If callable, should return a key for the mapping.
.. versionchanged:: 1.7 .. versionchanged:: 1.7
Callables are now supported for custom bucket types. Callables are now supported for custom bucket types.
""" """

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -143,7 +141,7 @@ class MissingRequiredArgument(UserInputError):
""" """
def __init__(self, param): def __init__(self, param):
self.param = param self.param = param
super().__init__('{0.name} is a required argument that is missing.'.format(param)) super().__init__(f'{param.name} is a required argument that is missing.')
class TooManyArguments(UserInputError): class TooManyArguments(UserInputError):
"""Exception raised when the command was passed too many arguments and its """Exception raised when the command was passed too many arguments and its
@ -229,7 +227,7 @@ class MemberNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Member "{}" not found.'.format(argument)) super().__init__(f'Member "{argument}" not found.')
class GuildNotFound(BadArgument): class GuildNotFound(BadArgument):
"""Exception raised when the guild provided was not found in the bot's cache. """Exception raised when the guild provided was not found in the bot's cache.
@ -245,7 +243,7 @@ class GuildNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Guild "{}" not found.'.format(argument)) super().__init__(f'Guild "{argument}" not found.')
class UserNotFound(BadArgument): class UserNotFound(BadArgument):
"""Exception raised when the user provided was not found in the bot's """Exception raised when the user provided was not found in the bot's
@ -262,7 +260,7 @@ class UserNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('User "{}" not found.'.format(argument)) super().__init__(f'User "{argument}" not found.')
class MessageNotFound(BadArgument): class MessageNotFound(BadArgument):
"""Exception raised when the message provided was not found in the channel. """Exception raised when the message provided was not found in the channel.
@ -278,7 +276,7 @@ class MessageNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Message "{}" not found.'.format(argument)) super().__init__(f'Message "{argument}" not found.')
class ChannelNotReadable(BadArgument): class ChannelNotReadable(BadArgument):
"""Exception raised when the bot does not have permission to read messages """Exception raised when the bot does not have permission to read messages
@ -295,7 +293,7 @@ class ChannelNotReadable(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__("Can't read messages in {}.".format(argument.mention)) super().__init__(f"Can't read messages in {argument.mention}.")
class ChannelNotFound(BadArgument): class ChannelNotFound(BadArgument):
"""Exception raised when the bot can not find the channel. """Exception raised when the bot can not find the channel.
@ -311,7 +309,7 @@ class ChannelNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Channel "{}" not found.'.format(argument)) super().__init__(f'Channel "{argument}" not found.')
class BadColourArgument(BadArgument): class BadColourArgument(BadArgument):
"""Exception raised when the colour is not valid. """Exception raised when the colour is not valid.
@ -327,7 +325,7 @@ class BadColourArgument(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Colour "{}" is invalid.'.format(argument)) super().__init__(f'Colour "{argument}" is invalid.')
BadColorArgument = BadColourArgument BadColorArgument = BadColourArgument
@ -345,7 +343,7 @@ class RoleNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Role "{}" not found.'.format(argument)) super().__init__(f'Role "{argument}" not found.')
class BadInviteArgument(BadArgument): class BadInviteArgument(BadArgument):
"""Exception raised when the invite is invalid or expired. """Exception raised when the invite is invalid or expired.
@ -371,7 +369,7 @@ class EmojiNotFound(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Emoji "{}" not found.'.format(argument)) super().__init__(f'Emoji "{argument}" not found.')
class PartialEmojiConversionFailure(BadArgument): class PartialEmojiConversionFailure(BadArgument):
"""Exception raised when the emoji provided does not match the correct """Exception raised when the emoji provided does not match the correct
@ -388,7 +386,7 @@ class PartialEmojiConversionFailure(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('Couldn\'t convert "{}" to PartialEmoji.'.format(argument)) super().__init__(f'Couldn\'t convert "{argument}" to PartialEmoji.')
class BadBoolArgument(BadArgument): class BadBoolArgument(BadArgument):
"""Exception raised when a boolean argument was not convertable. """Exception raised when a boolean argument was not convertable.
@ -404,7 +402,7 @@ class BadBoolArgument(BadArgument):
""" """
def __init__(self, argument): def __init__(self, argument):
self.argument = argument self.argument = argument
super().__init__('{} is not a recognised boolean option'.format(argument)) super().__init__(f'{argument} is not a recognised boolean option')
class DisabledCommand(CommandError): class DisabledCommand(CommandError):
"""Exception raised when the command being invoked is disabled. """Exception raised when the command being invoked is disabled.
@ -444,7 +442,7 @@ class CommandOnCooldown(CommandError):
def __init__(self, cooldown, retry_after): def __init__(self, cooldown, retry_after):
self.cooldown = cooldown self.cooldown = cooldown
self.retry_after = retry_after self.retry_after = retry_after
super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after)) super().__init__(f'You are on cooldown. Try again in {retry_after:.2f}s')
class MaxConcurrencyReached(CommandError): class MaxConcurrencyReached(CommandError):
"""Exception raised when the command being invoked has reached its maximum concurrency. """Exception raised when the command being invoked has reached its maximum concurrency.
@ -466,7 +464,7 @@ class MaxConcurrencyReached(CommandError):
suffix = 'per %s' % name if per.name != 'default' else 'globally' suffix = 'per %s' % name if per.name != 'default' else 'globally'
plural = '%s times %s' if number > 1 else '%s time %s' plural = '%s times %s' if number > 1 else '%s time %s'
fmt = plural % (number, suffix) fmt = plural % (number, suffix)
super().__init__('Too many people using this command. It can only be used {} concurrently.'.format(fmt)) super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
class MissingRole(CheckFailure): class MissingRole(CheckFailure):
"""Exception raised when the command invoker lacks a role to run a command. """Exception raised when the command invoker lacks a role to run a command.
@ -483,7 +481,7 @@ class MissingRole(CheckFailure):
""" """
def __init__(self, missing_role): def __init__(self, missing_role):
self.missing_role = missing_role self.missing_role = missing_role
message = 'Role {0!r} is required to run this command.'.format(missing_role) message = f'Role {missing_role!r} is required to run this command.'
super().__init__(message) super().__init__(message)
class BotMissingRole(CheckFailure): class BotMissingRole(CheckFailure):
@ -501,7 +499,7 @@ class BotMissingRole(CheckFailure):
""" """
def __init__(self, missing_role): def __init__(self, missing_role):
self.missing_role = missing_role self.missing_role = missing_role
message = 'Bot requires the role {0!r} to run this command'.format(missing_role) message = f'Bot requires the role {missing_role!r} to run this command'
super().__init__(message) super().__init__(message)
class MissingAnyRole(CheckFailure): class MissingAnyRole(CheckFailure):
@ -521,14 +519,14 @@ class MissingAnyRole(CheckFailure):
def __init__(self, missing_roles): def __init__(self, missing_roles):
self.missing_roles = missing_roles self.missing_roles = missing_roles
missing = ["'{}'".format(role) for role in missing_roles] missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2: if len(missing) > 2:
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1]) fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
else: else:
fmt = ' or '.join(missing) fmt = ' or '.join(missing)
message = "You are missing at least one of the required roles: {}".format(fmt) message = f"You are missing at least one of the required roles: {fmt}"
super().__init__(message) super().__init__(message)
@ -550,14 +548,14 @@ class BotMissingAnyRole(CheckFailure):
def __init__(self, missing_roles): def __init__(self, missing_roles):
self.missing_roles = missing_roles self.missing_roles = missing_roles
missing = ["'{}'".format(role) for role in missing_roles] missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2: if len(missing) > 2:
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1]) fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
else: else:
fmt = ' or '.join(missing) fmt = ' or '.join(missing)
message = "Bot is missing at least one of the required roles: {}".format(fmt) message = f"Bot is missing at least one of the required roles: {fmt}"
super().__init__(message) super().__init__(message)
class NSFWChannelRequired(CheckFailure): class NSFWChannelRequired(CheckFailure):
@ -574,7 +572,7 @@ class NSFWChannelRequired(CheckFailure):
""" """
def __init__(self, channel): def __init__(self, channel):
self.channel = channel self.channel = channel
super().__init__("Channel '{}' needs to be NSFW for this command to work.".format(channel)) super().__init__(f"Channel '{channel}' needs to be NSFW for this command to work.")
class MissingPermissions(CheckFailure): class MissingPermissions(CheckFailure):
"""Exception raised when the command invoker lacks permissions to run a """Exception raised when the command invoker lacks permissions to run a
@ -596,7 +594,7 @@ class MissingPermissions(CheckFailure):
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else: else:
fmt = ' and '.join(missing) fmt = ' and '.join(missing)
message = 'You are missing {} permission(s) to run this command.'.format(fmt) message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args) super().__init__(message, *args)
class BotMissingPermissions(CheckFailure): class BotMissingPermissions(CheckFailure):
@ -619,7 +617,7 @@ class BotMissingPermissions(CheckFailure):
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else: else:
fmt = ' and '.join(missing) fmt = ' and '.join(missing)
message = 'Bot requires {} permission(s) to run this command.'.format(fmt) message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args) super().__init__(message, *args)
class BadUnionArgument(UserInputError): class BadUnionArgument(UserInputError):
@ -654,7 +652,7 @@ class BadUnionArgument(UserInputError):
else: else:
fmt = ' or '.join(to_string) fmt = ' or '.join(to_string)
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt)) super().__init__(f'Could not convert "{param.name}" into {fmt}.')
class ArgumentParsingError(UserInputError): class ArgumentParsingError(UserInputError):
"""An exception raised when the parser fails to parse a user's input. """An exception raised when the parser fails to parse a user's input.
@ -678,7 +676,7 @@ class UnexpectedQuoteError(ArgumentParsingError):
""" """
def __init__(self, quote): def __init__(self, quote):
self.quote = quote self.quote = quote
super().__init__('Unexpected quote mark, {0!r}, in non-quoted string'.format(quote)) super().__init__(f'Unexpected quote mark, {quote!r}, in non-quoted string')
class InvalidEndOfQuotedStringError(ArgumentParsingError): class InvalidEndOfQuotedStringError(ArgumentParsingError):
"""An exception raised when a space is expected after the closing quote in a string """An exception raised when a space is expected after the closing quote in a string
@ -693,7 +691,7 @@ class InvalidEndOfQuotedStringError(ArgumentParsingError):
""" """
def __init__(self, char): def __init__(self, char):
self.char = char self.char = char
super().__init__('Expected space after closing quotation but received {0!r}'.format(char)) super().__init__(f'Expected space after closing quotation but received {char!r}')
class ExpectedClosingQuoteError(ArgumentParsingError): class ExpectedClosingQuoteError(ArgumentParsingError):
"""An exception raised when a quote character is expected but not found. """An exception raised when a quote character is expected but not found.
@ -708,7 +706,7 @@ class ExpectedClosingQuoteError(ArgumentParsingError):
def __init__(self, close_quote): def __init__(self, close_quote):
self.close_quote = close_quote self.close_quote = close_quote
super().__init__('Expected closing {}.'.format(close_quote)) super().__init__(f'Expected closing {close_quote}.')
class ExtensionError(DiscordException): class ExtensionError(DiscordException):
"""Base exception for extension related errors. """Base exception for extension related errors.
@ -722,7 +720,7 @@ class ExtensionError(DiscordException):
""" """
def __init__(self, message=None, *args, name): def __init__(self, message=None, *args, name):
self.name = name self.name = name
message = message or 'Extension {!r} had an error.'.format(name) message = message or f'Extension {name!r} had an error.'
# clean-up @everyone and @here mentions # clean-up @everyone and @here mentions
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere') m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
super().__init__(m, *args) super().__init__(m, *args)
@ -733,7 +731,7 @@ class ExtensionAlreadyLoaded(ExtensionError):
This inherits from :exc:`ExtensionError` This inherits from :exc:`ExtensionError`
""" """
def __init__(self, name): def __init__(self, name):
super().__init__('Extension {!r} is already loaded.'.format(name), name=name) super().__init__(f'Extension {name!r} is already loaded.', name=name)
class ExtensionNotLoaded(ExtensionError): class ExtensionNotLoaded(ExtensionError):
"""An exception raised when an extension was not loaded. """An exception raised when an extension was not loaded.
@ -741,7 +739,7 @@ class ExtensionNotLoaded(ExtensionError):
This inherits from :exc:`ExtensionError` This inherits from :exc:`ExtensionError`
""" """
def __init__(self, name): def __init__(self, name):
super().__init__('Extension {!r} has not been loaded.'.format(name), name=name) super().__init__(f'Extension {name!r} has not been loaded.', name=name)
class NoEntryPointError(ExtensionError): class NoEntryPointError(ExtensionError):
"""An exception raised when an extension does not have a ``setup`` entry point function. """An exception raised when an extension does not have a ``setup`` entry point function.
@ -749,7 +747,7 @@ class NoEntryPointError(ExtensionError):
This inherits from :exc:`ExtensionError` This inherits from :exc:`ExtensionError`
""" """
def __init__(self, name): def __init__(self, name):
super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name) super().__init__(f"Extension {name!r} has no 'setup' function.", name=name)
class ExtensionFailed(ExtensionError): class ExtensionFailed(ExtensionError):
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point. """An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
@ -808,4 +806,4 @@ class CommandRegistrationError(ClientException):
self.name = name self.name = name
self.alias_conflict = alias_conflict self.alias_conflict = alias_conflict
type_ = 'alias' if alias_conflict else 'command' type_ = 'alias' if alias_conflict else 'command'
super().__init__('The {} {} is already an existing command or alias.'.format(type_, name)) super().__init__(f'The {type_} {name} is already an existing command or alias.')

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -132,7 +130,7 @@ class Paginator:
""" """
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
if len(line) > max_page_size: if len(line) > max_page_size:
raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size)) raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len: if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
self.close_page() self.close_page()
@ -386,8 +384,9 @@ class HelpCommand:
# consider this to be an *incredibly* strange use case. I'd rather go # consider this to be an *incredibly* strange use case. I'd rather go
# for this common use case rather than waste performance for the # for this common use case rather than waste performance for the
# odd one. # odd one.
pattern = re.compile(r"<@!?%s>" % user.id) pattern = re.compile(fr"<@!?{user.id}>")
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.context.prefix) display_name = user.display_name.replace('\\', r'\\')
return pattern.sub('@' + display_name, self.context.prefix)
@property @property
def invoked_with(self): def invoked_with(self):
@ -436,14 +435,14 @@ class HelpCommand:
if len(command.aliases) > 0: if len(command.aliases) > 0:
aliases = '|'.join(command.aliases) aliases = '|'.join(command.aliases)
fmt = '[%s|%s]' % (command.name, aliases) fmt = f'[{command.name}|{aliases}]'
if parent_sig: if parent_sig:
fmt = parent_sig + ' ' + fmt fmt = parent_sig + ' ' + fmt
alias = fmt alias = fmt
else: else:
alias = command.name if not parent_sig else parent_sig + ' ' + command.name alias = command.name if not parent_sig else parent_sig + ' ' + command.name
return '%s%s %s' % (self.clean_prefix, alias, command.signature) return f'{self.clean_prefix}{alias} {command.signature}'
def remove_mentions(self, string): def remove_mentions(self, string):
"""Removes mentions from the string to prevent abuse. """Removes mentions from the string to prevent abuse.
@ -506,7 +505,7 @@ class HelpCommand:
:class:`str` :class:`str`
The string to use when a command has not been found. The string to use when a command has not been found.
""" """
return 'No command called "{}" found.'.format(string) return f'No command called "{string}" found.'
def subcommand_not_found(self, command, string): def subcommand_not_found(self, command, string):
"""|maybecoro| """|maybecoro|
@ -535,8 +534,8 @@ class HelpCommand:
The string to use when the command did not have the subcommand requested. The string to use when the command did not have the subcommand requested.
""" """
if isinstance(command, Group) and len(command.all_commands) > 0: if isinstance(command, Group) and len(command.all_commands) > 0:
return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string) return f'Command "{command.qualified_name}" has no subcommand named {string}'
return 'Command "{0.qualified_name}" has no subcommands.'.format(command) return f'Command "{command.qualified_name}" has no subcommands.'
async def filter_commands(self, commands, *, sort=False, key=None): async def filter_commands(self, commands, *, sort=False, key=None):
"""|coro| """|coro|
@ -941,8 +940,8 @@ class DefaultHelpCommand(HelpCommand):
def get_ending_note(self): def get_ending_note(self):
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes.""" """:class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
command_name = self.invoked_with command_name = self.invoked_with
return "Type {0}{1} command for more info on a command.\n" \ return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name) f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
def add_indented_commands(self, commands, *, heading, max_size=None): def add_indented_commands(self, commands, *, heading, max_size=None):
"""Indents a list of commands after the specified heading. """Indents a list of commands after the specified heading.
@ -977,7 +976,7 @@ class DefaultHelpCommand(HelpCommand):
for command in commands: for command in commands:
name = command.name name = command.name
width = max_size - (get_width(name) - len(name)) width = max_size - (get_width(name) - len(name))
entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width) entry = f'{self.indent * " "}{name:<{width}} {command.short_doc}'
self.paginator.add_line(self.shorten_text(entry)) self.paginator.add_line(self.shorten_text(entry))
async def send_pages(self): async def send_pages(self):
@ -1030,7 +1029,7 @@ class DefaultHelpCommand(HelpCommand):
# <description> portion # <description> portion
self.paginator.add_line(bot.description, empty=True) self.paginator.add_line(bot.description, empty=True)
no_category = '\u200b{0.no_category}:'.format(self) no_category = f'\u200b{self.no_category}:'
def get_category(command, *, no_category=no_category): def get_category(command, *, no_category=no_category):
cog = command.cog cog = command.cog
return cog.qualified_name + ':' if cog is not None else no_category return cog.qualified_name + ':' if cog is not None else no_category
@ -1154,7 +1153,7 @@ class MinimalHelpCommand(HelpCommand):
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name) "You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
def get_command_signature(self, command): def get_command_signature(self, command):
return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature) return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
def get_ending_note(self): def get_ending_note(self):
"""Return the help command's ending note. This is mainly useful to override for i18n purposes. """Return the help command's ending note. This is mainly useful to override for i18n purposes.
@ -1186,7 +1185,7 @@ class MinimalHelpCommand(HelpCommand):
if commands: if commands:
# U+2002 Middle Dot # U+2002 Middle Dot
joined = '\u2002'.join(c.name for c in commands) joined = '\u2002'.join(c.name for c in commands)
self.paginator.add_line('__**%s**__' % heading) self.paginator.add_line(f'__**{heading}**__')
self.paginator.add_line(joined) self.paginator.add_line(joined)
def add_subcommand_formatting(self, command): def add_subcommand_formatting(self, command):
@ -1220,7 +1219,7 @@ class MinimalHelpCommand(HelpCommand):
aliases: Sequence[:class:`str`] aliases: Sequence[:class:`str`]
A list of aliases to format. A list of aliases to format.
""" """
self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True) self.paginator.add_line(f'**{self.aliases_heading}** {", ".join(aliases)}', empty=True)
def add_command_formatting(self, command): def add_command_formatting(self, command):
"""A utility function to format commands and groups. """A utility function to format commands and groups.
@ -1273,7 +1272,7 @@ class MinimalHelpCommand(HelpCommand):
if note: if note:
self.paginator.add_line(note, empty=True) self.paginator.add_line(note, empty=True)
no_category = '\u200b{0.no_category}'.format(self) no_category = f'\u200b{self.no_category}'
def get_category(command, *, no_category=no_category): def get_category(command, *, no_category=no_category):
cog = command.cog cog = command.cog
return cog.qualified_name if cog is not None else no_category return cog.qualified_name if cog is not None else no_category
@ -1306,7 +1305,7 @@ class MinimalHelpCommand(HelpCommand):
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands) filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
if filtered: if filtered:
self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading)) self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
for command in filtered: for command in filtered:
self.add_subcommand_formatting(command) self.add_subcommand_formatting(command)
@ -1326,7 +1325,7 @@ class MinimalHelpCommand(HelpCommand):
if note: if note:
self.paginator.add_line(note, empty=True) self.paginator.add_line(note, empty=True)
self.paginator.add_line('**%s**' % self.commands_heading) self.paginator.add_line(f'**{self.commands_heading}**')
for command in filtered: for command in filtered:
self.add_subcommand_formatting(command) self.add_subcommand_formatting(command)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -37,6 +35,10 @@ from discord.backoff import ExponentialBackoff
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__all__ = (
'loop',
)
class Loop: class Loop:
"""A background task helper that abstracts the loop and reconnection logic for you. """A background task helper that abstracts the loop and reconnection logic for you.
@ -289,9 +291,9 @@ class Loop:
for exc in exceptions: for exc in exceptions:
if not inspect.isclass(exc): if not inspect.isclass(exc):
raise TypeError('{0!r} must be a class.'.format(exc)) raise TypeError(f'{exc!r} must be a class.')
if not issubclass(exc, BaseException): if not issubclass(exc, BaseException):
raise TypeError('{0!r} must inherit from BaseException.'.format(exc)) raise TypeError(f'{exc!r} must inherit from BaseException.')
self._valid_exception = (*self._valid_exception, *exceptions) self._valid_exception = (*self._valid_exception, *exceptions)
@ -345,7 +347,7 @@ class Loop:
async def _error(self, *args): async def _error(self, *args):
exception = args[-1] exception = args[-1]
print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr) print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
def before_loop(self, coro): def before_loop(self, coro):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
import os.path import os.path
import io import io
__all__ = (
'File',
)
class File: class File:
r"""A parameter object used for :meth:`abc.Messageable.send` r"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects. for sending file objects.
@ -65,7 +67,7 @@ class File:
if isinstance(fp, io.IOBase): if isinstance(fp, io.IOBase):
if not (fp.seekable() and fp.readable()): if not (fp.seekable() and fp.readable()):
raise ValueError('File buffer {!r} must be seekable and readable'.format(fp)) raise ValueError(f'File buffer {fp!r} must be seekable and readable')
self.fp = fp self.fp = fp
self._original_pos = fp.tell() self._original_pos = fp.tell()
self._owner = False self._owner = False

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations
from typing import Any, Callable, ClassVar, Dict, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, overload
from .enums import UserFlags from .enums import UserFlags
__all__ = ( __all__ = (
@ -34,27 +36,38 @@ __all__ = (
'MemberCacheFlags', 'MemberCacheFlags',
) )
class flag_value: FV = TypeVar('FV', bound='flag_value')
def __init__(self, func): BF = TypeVar('BF', bound='BaseFlags')
class flag_value(Generic[BF]):
def __init__(self, func: Callable[[Any], int]):
self.flag = func(None) self.flag = func(None)
self.__doc__ = func.__doc__ self.__doc__ = func.__doc__
def __get__(self, instance, owner): @overload
def __get__(self: FV, instance: None, owner: Type[BF]) -> FV:
...
@overload
def __get__(self, instance: BF, owner: Type[BF]) -> bool:
...
def __get__(self, instance: Optional[BF], owner: Type[BF]) -> Any:
if instance is None: if instance is None:
return self return self
return instance._has_flag(self.flag) return instance._has_flag(self.flag)
def __set__(self, instance, value): def __set__(self, instance: BF, value: bool) -> None:
instance._set_flag(self.flag, value) instance._set_flag(self.flag, value)
def __repr__(self): def __repr__(self):
return '<flag_value flag={.flag!r}>'.format(self) return f'<flag_value flag={self.flag!r}>'
class alias_flag_value(flag_value): class alias_flag_value(flag_value):
pass pass
def fill_with_flags(*, inverted=False): def fill_with_flags(*, inverted: bool = False):
def decorator(cls): def decorator(cls: Type[BF]):
cls.VALID_FLAGS = { cls.VALID_FLAGS = {
name: value.flag name: value.flag
for name, value in cls.__dict__.items() for name, value in cls.__dict__.items()
@ -72,13 +85,18 @@ def fill_with_flags(*, inverted=False):
# n.b. flags must inherit from this and use the decorator above # n.b. flags must inherit from this and use the decorator above
class BaseFlags: class BaseFlags:
VALID_FLAGS: ClassVar[Dict[str, int]]
DEFAULT_VALUE: ClassVar[int]
value: int
__slots__ = ('value',) __slots__ = ('value',)
def __init__(self, **kwargs): def __init__(self, **kwargs: bool):
self.value = self.DEFAULT_VALUE self.value = self.DEFAULT_VALUE
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_FLAGS: if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key) raise TypeError(f'{key!r} is not a valid flag name.')
setattr(self, key, value) setattr(self, key, value)
@classmethod @classmethod
@ -87,19 +105,19 @@ class BaseFlags:
self.value = value self.value = value
return self return self
def __eq__(self, other): def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and self.value == other.value return isinstance(other, self.__class__) and self.value == other.value
def __ne__(self, other): def __ne__(self, other: Any) -> bool:
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self): def __hash__(self) -> int:
return hash(self.value) return hash(self.value)
def __repr__(self): def __repr__(self) -> str:
return '<%s value=%s>' % (self.__class__.__name__, self.value) return f'<{self.__class__.__name__} value={self.value}>'
def __iter__(self): def __iter__(self) -> Iterator[Tuple[str, bool]]:
for name, value in self.__class__.__dict__.items(): for name, value in self.__class__.__dict__.items():
if isinstance(value, alias_flag_value): if isinstance(value, alias_flag_value):
continue continue
@ -107,16 +125,16 @@ class BaseFlags:
if isinstance(value, flag_value): if isinstance(value, flag_value):
yield (name, self._has_flag(value.flag)) yield (name, self._has_flag(value.flag))
def _has_flag(self, o): def _has_flag(self, o: int) -> bool:
return (self.value & o) == o return (self.value & o) == o
def _set_flag(self, o, toggle): def _set_flag(self, o: int, toggle: bool) -> None:
if toggle is True: if toggle is True:
self.value |= o self.value |= o
elif toggle is False: elif toggle is False:
self.value &= ~o self.value &= ~o
else: else:
raise TypeError('Value to set for %s must be a bool.' % self.__class__.__name__) raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
@fill_with_flags(inverted=True) @fill_with_flags(inverted=True)
class SystemChannelFlags(BaseFlags): class SystemChannelFlags(BaseFlags):
@ -152,6 +170,7 @@ class SystemChannelFlags(BaseFlags):
representing the currently available flags. You should query representing the currently available flags. You should query
flags via the properties rather than using this raw value. flags via the properties rather than using this raw value.
""" """
__slots__ = () __slots__ = ()
# For some reason the flags for system channels are "inverted" # For some reason the flags for system channels are "inverted"
@ -159,10 +178,10 @@ class SystemChannelFlags(BaseFlags):
# Since this is counter-intuitive from an API perspective and annoying # Since this is counter-intuitive from an API perspective and annoying
# these will be inverted automatically # these will be inverted automatically
def _has_flag(self, o): def _has_flag(self, o: int) -> bool:
return (self.value & o) != o return (self.value & o) != o
def _set_flag(self, o, toggle): def _set_flag(self, o: int, toggle: bool) -> None:
if toggle is True: if toggle is True:
self.value &= ~o self.value &= ~o
elif toggle is False: elif toggle is False:
@ -212,6 +231,7 @@ class MessageFlags(BaseFlags):
representing the currently available flags. You should query representing the currently available flags. You should query
flags via the properties rather than using this raw value. flags via the properties rather than using this raw value.
""" """
__slots__ = () __slots__ = ()
@flag_value @flag_value
@ -348,7 +368,7 @@ class PublicUserFlags(BaseFlags):
""" """
return UserFlags.verified_bot_developer.value return UserFlags.verified_bot_developer.value
def all(self): def all(self) -> List[UserFlags]:
"""List[:class:`UserFlags`]: Returns all public flags the user has.""" """List[:class:`UserFlags`]: Returns all public flags the user has."""
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)] return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
@ -395,11 +415,11 @@ class Intents(BaseFlags):
__slots__ = () __slots__ = ()
def __init__(self, **kwargs): def __init__(self, **kwargs: bool):
self.value = self.DEFAULT_VALUE self.value = self.DEFAULT_VALUE
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_FLAGS: if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key) raise TypeError(f'{key!r} is not a valid flag name.')
setattr(self, key, value) setattr(self, key, value)
@classmethod @classmethod
@ -419,7 +439,7 @@ class Intents(BaseFlags):
return self return self
@classmethod @classmethod
def all(cls): def all(cls: Type[Intents]) -> Intents:
"""A factory method that creates a :class:`Intents` with everything enabled.""" """A factory method that creates a :class:`Intents` with everything enabled."""
bits = max(cls.VALID_FLAGS.values()).bit_length() bits = max(cls.VALID_FLAGS.values()).bit_length()
value = (1 << bits) - 1 value = (1 << bits) - 1
@ -428,14 +448,14 @@ class Intents(BaseFlags):
return self return self
@classmethod @classmethod
def none(cls): def none(cls: Type[Intents]) -> Intents:
"""A factory method that creates a :class:`Intents` with everything disabled.""" """A factory method that creates a :class:`Intents` with everything disabled."""
self = cls.__new__(cls) self = cls.__new__(cls)
self.value = self.DEFAULT_VALUE self.value = self.DEFAULT_VALUE
return self return self
@classmethod @classmethod
def default(cls): def default(cls: Type[Intents]) -> Intents:
"""A factory method that creates a :class:`Intents` with everything enabled """A factory method that creates a :class:`Intents` with everything enabled
except :attr:`presences` and :attr:`members`. except :attr:`presences` and :attr:`members`.
""" """
@ -844,16 +864,16 @@ class MemberCacheFlags(BaseFlags):
__slots__ = () __slots__ = ()
def __init__(self, **kwargs): def __init__(self, **kwargs: bool):
bits = max(self.VALID_FLAGS.values()).bit_length() bits = max(self.VALID_FLAGS.values()).bit_length()
self.value = (1 << bits) - 1 self.value = (1 << bits) - 1
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_FLAGS: if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key) raise TypeError(f'{key!r} is not a valid flag name.')
setattr(self, key, value) setattr(self, key, value)
@classmethod @classmethod
def all(cls): def all(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
"""A factory method that creates a :class:`MemberCacheFlags` with everything enabled.""" """A factory method that creates a :class:`MemberCacheFlags` with everything enabled."""
bits = max(cls.VALID_FLAGS.values()).bit_length() bits = max(cls.VALID_FLAGS.values()).bit_length()
value = (1 << bits) - 1 value = (1 << bits) - 1
@ -862,7 +882,7 @@ class MemberCacheFlags(BaseFlags):
return self return self
@classmethod @classmethod
def none(cls): def none(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
"""A factory method that creates a :class:`MemberCacheFlags` with everything disabled.""" """A factory method that creates a :class:`MemberCacheFlags` with everything disabled."""
self = cls.__new__(cls) self = cls.__new__(cls)
self.value = self.DEFAULT_VALUE self.value = self.DEFAULT_VALUE
@ -905,7 +925,7 @@ class MemberCacheFlags(BaseFlags):
return 4 return 4
@classmethod @classmethod
def from_intents(cls, intents): def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
"""A factory method that creates a :class:`MemberCacheFlags` based on """A factory method that creates a :class:`MemberCacheFlags` based on
the currently selected :class:`Intents`. the currently selected :class:`Intents`.
@ -933,7 +953,7 @@ class MemberCacheFlags(BaseFlags):
return self return self
def _verify_intents(self, intents): def _verify_intents(self, intents: Intents):
if self.online and not intents.presences: if self.online and not intents.presences:
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled') raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -162,8 +160,8 @@ class KeepAliveHandler(threading.Thread):
except KeyError: except KeyError:
msg = self.block_msg msg = self.block_msg
else: else:
stack = traceback.format_stack(frame) stack = ''.join(traceback.format_stack(frame))
msg = '%s\nLoop thread traceback (most recent call last):\n%s' % (self.block_msg, ''.join(stack)) msg = f'{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}'
log.warning(msg, self.shard_id, total) log.warning(msg, self.shard_id, total)
except Exception: except Exception:
@ -380,9 +378,6 @@ class DiscordWebSocket:
} }
} }
if not self._connection.is_bot:
payload['d']['synced_guilds'] = []
if self.shard_id is not None and self.shard_count is not None: if self.shard_id is not None and self.shard_count is not None:
payload['d']['shard'] = [self.shard_id, self.shard_count] payload['d']['shard'] = [self.shard_id, self.shard_count]
@ -624,13 +619,6 @@ class DiscordWebSocket:
log.debug('Sending "%s" to change status', sent) log.debug('Sending "%s" to change status', sent)
await self.send(sent) await self.send(sent)
async def request_sync(self, guild_ids):
payload = {
'op': self.GUILD_SYNC,
'd': list(guild_ids)
}
await self.send_as_json(payload)
async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None): async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None):
payload = { payload = {
'op': self.REQUEST_MEMBERS, 'op': self.REQUEST_MEMBERS,

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -46,6 +44,9 @@ from .asset import Asset
from .flags import SystemChannelFlags from .flags import SystemChannelFlags
from .integrations import Integration from .integrations import Integration
__all__ = (
'Guild',
)
BanEntry = namedtuple('BanEntry', 'reason user') BanEntry = namedtuple('BanEntry', 'reason user')
_GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize') _GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize')
@ -212,11 +213,14 @@ class Guild(Hashable):
def __repr__(self): def __repr__(self):
attrs = ( attrs = (
'id', 'name', 'shard_id', 'chunked' ('id', self.id),
('name', self.name),
('shard_id', self.shard_id),
('chunked', self.chunked),
('member_count', getattr(self, '_member_count', None)),
) )
resolved = ['%s=%r' % (attr, getattr(self, attr)) for attr in attrs] inner = ' '.join('%s=%r' % t for t in attrs)
resolved.append('member_count=%r' % getattr(self, '_member_count', None)) return f'<Guild {inner}>'
return '<Guild %s>' % ' '.join(resolved)
def _update_voice_state(self, data, channel_id): def _update_voice_state(self, data, channel_id):
user_id = int(data['user_id']) user_id = int(data['user_id'])
@ -1352,7 +1356,8 @@ class Guild(Hashable):
Pass ``None`` to fetch all members. Note that this is potentially slow. Pass ``None`` to fetch all members. Note that this is potentially slow.
after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve members after this date or object. Retrieve members after this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
Raises Raises
------ ------
@ -1560,7 +1565,7 @@ class Guild(Hashable):
""" """
if not isinstance(days, int): if not isinstance(days, int):
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
if roles: if roles:
roles = [str(role.id) for role in roles] roles = [str(role.id) for role in roles]
@ -1646,7 +1651,7 @@ class Guild(Hashable):
""" """
if not isinstance(days, int): if not isinstance(days, int):
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
if roles: if roles:
roles = [str(role.id) for role in roles] roles = [str(role.id) for role in roles]
@ -1949,7 +1954,7 @@ class Guild(Hashable):
valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable')
for key in fields: for key in fields:
if key not in valid_keys: if key not in valid_keys:
raise InvalidArgument('%r is not a valid field.' % key) raise InvalidArgument(f'{key!r} is not a valid field.')
data = await self._state.http.create_role(self.id, reason=reason, **fields) data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state) role = Role(guild=self, data=data, state=self._state)
@ -2142,29 +2147,6 @@ class Guild(Hashable):
payload['max_age'] = 0 payload['max_age'] = 0
return Invite(state=self._state, data=payload) return Invite(state=self._state, data=payload)
@utils.deprecated()
def ack(self):
"""|coro|
Marks every message in this guild as read.
The user must not be a bot user.
.. deprecated:: 1.7
Raises
-------
HTTPException
Acking failed.
ClientException
You must not be a bot user.
"""
state = self._state
if state.is_bot:
raise ClientException('Must not be a bot account to ack messages.')
return state.http.ack_guild(self.id)
def audit_logs(self, *, limit=100, before=None, after=None, oldest_first=None, user=None, action=None): def audit_logs(self, *, limit=100, before=None, after=None, oldest_first=None, user=None, action=None):
"""Returns an :class:`AsyncIterator` that enables receiving the guild's audit logs. """Returns an :class:`AsyncIterator` that enables receiving the guild's audit logs.
@ -2194,10 +2176,12 @@ class Guild(Hashable):
The number of entries to retrieve. If ``None`` retrieve all entries. The number of entries to retrieve. If ``None`` retrieve all entries.
before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
Retrieve entries before this date or entry. Retrieve entries before this date or entry.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
Retrieve entries after this date or entry. Retrieve entries after this date or entry.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
oldest_first: :class:`bool` oldest_first: :class:`bool`
If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if
``after`` is specified, otherwise ``False``. ``after`` is specified, otherwise ``False``.

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -30,6 +28,11 @@ from .user import User
from .errors import InvalidArgument from .errors import InvalidArgument
from .enums import try_enum, ExpireBehaviour from .enums import try_enum, ExpireBehaviour
__all__ = (
'IntegrationAccount',
'Integration',
)
class IntegrationAccount: class IntegrationAccount:
"""Represents an integration account. """Represents an integration account.
@ -84,7 +87,7 @@ class Integration:
account: :class:`IntegrationAccount` account: :class:`IntegrationAccount`
The integration account information. The integration account information.
synced_at: :class:`datetime.datetime` synced_at: :class:`datetime.datetime`
When the integration was last synced. An aware UTC datetime representing when the integration was last synced.
""" """
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type', __slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
@ -186,7 +189,7 @@ class Integration:
Syncing the integration failed. Syncing the integration failed.
""" """
await self._state.http.sync_integration(self.guild.id, self.id) await self._state.http.sync_integration(self.guild.id, self.id)
self.synced_at = datetime.datetime.utcnow() self.synced_at = datetime.datetime.now(datetime.timezone.utc)
async def delete(self): async def delete(self):
"""|coro| """|coro|

104
discord/interactions.py Normal file
View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from . import utils
from .enums import try_enum, InteractionType
__all__ = (
'Interaction',
)
class Interaction:
"""Represents a Discord interaction.
An interaction happens when a user does an action that needs to
be notified. Current examples are slash commands but future examples
include forms and buttons.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The interaction's ID.
type: :class:`InteractionType`
The interaction type.
guild_id: Optional[:class:`int`]
The guild ID the interaction was sent from.
channel_id: Optional[:class:`int`]
The channel ID the interaction was sent from.
application_id: :class:`int`
The application ID that the interaction was for.
user: Optional[Union[:class:`User`, :class:`Member`]]
The user or member that sent the interaction.
token: :class:`str`
The token to continue the interaction. These are valid
for 15 minutes.
"""
__slots__ = (
'id',
'type',
'guild_id',
'channel_id',
'data',
'application_id',
'user',
'token',
'version',
'_state',
)
def __init__(self, *, data, state=None):
self._state = state
self._from_data(data)
def _from_data(self, data):
self.id = int(data['id'])
self.type = try_enum(InteractionType, data['type'])
self.data = data.get('data')
self.token = data['token']
self.version = data['version']
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.application_id = utils._get_as_snowflake(data, 'application_id')
@property
def guild(self):
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
return self._state and self._state.get_guild(self.guild_id)
@property
def channel(self):
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
Note that due to a Discord limitation, DM channels are not resolved since there is
no data to complete them.
"""
guild = self.guild
return guild and guild.get_channel(self.channel_id)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -30,6 +28,12 @@ from .object import Object
from .mixins import Hashable from .mixins import Hashable
from .enums import ChannelType, VerificationLevel, try_enum from .enums import ChannelType, VerificationLevel, try_enum
__all__ = (
'PartialInviteChannel',
'PartialInviteGuild',
'Invite',
)
class PartialInviteChannel: class PartialInviteChannel:
"""Represents a "partial" invite channel. """Represents a "partial" invite channel.
@ -80,7 +84,7 @@ class PartialInviteChannel:
@property @property
def mention(self): def mention(self):
""":class:`str`: The string that allows you to mention the channel.""" """:class:`str`: The string that allows you to mention the channel."""
return '<#%s>' % self.id return f'<#{self.id}>'
@property @property
def created_at(self): def created_at(self):
@ -267,7 +271,7 @@ class Invite(Hashable):
revoked: :class:`bool` revoked: :class:`bool`
Indicates if the invite has been revoked. Indicates if the invite has been revoked.
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
A datetime object denoting the time the invite was created. An aware UTC datetime object denoting the time the invite was created.
temporary: :class:`bool` temporary: :class:`bool`
Indicates that the invite grants temporary membership. Indicates that the invite grants temporary membership.
If ``True``, members who joined via this invite will be kicked upon disconnect. If ``True``, members who joined via this invite will be kicked upon disconnect.

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,20 +22,43 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations
import asyncio import asyncio
import datetime import datetime
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
from .errors import NoMoreItems from .errors import NoMoreItems
from .utils import time_snowflake, maybe_coroutine from .utils import time_snowflake, maybe_coroutine
from .object import Object from .object import Object
from .audit_logs import AuditLogEntry from .audit_logs import AuditLogEntry
__all__ = (
'ReactionIterator',
'HistoryIterator',
'AuditLogIterator',
'GuildIterator',
'MemberIterator',
)
if TYPE_CHECKING:
from .member import Member
from .user import User
from .message import Message
from .audit_logs import AuditLogEntry
from .guild import Guild
T = TypeVar('T')
OT = TypeVar('OT')
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
OLDEST_OBJECT = Object(id=0) OLDEST_OBJECT = Object(id=0)
class _AsyncIterator: class _AsyncIterator(AsyncIterator[T]):
__slots__ = () __slots__ = ()
def get(self, **attrs): def get(self, **attrs: Any) -> Optional[T]:
def predicate(elem): def predicate(elem):
for attr, val in attrs.items(): for attr, val in attrs.items():
nested = attr.split('__') nested = attr.split('__')
@ -51,7 +72,7 @@ class _AsyncIterator:
return self.find(predicate) return self.find(predicate)
async def find(self, predicate): async def find(self, predicate: _Predicate[T]) -> Optional[T]:
while True: while True:
try: try:
elem = await self.next() elem = await self.next()
@ -62,47 +83,35 @@ class _AsyncIterator:
if ret: if ret:
return elem return elem
def chunk(self, max_size): def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
if max_size <= 0: if max_size <= 0:
raise ValueError('async iterator chunk sizes must be greater than 0.') raise ValueError('async iterator chunk sizes must be greater than 0.')
return _ChunkedAsyncIterator(self, max_size) return _ChunkedAsyncIterator(self, max_size)
def map(self, func): def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
return _MappedAsyncIterator(self, func) return _MappedAsyncIterator(self, func)
def filter(self, predicate): def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
return _FilteredAsyncIterator(self, predicate) return _FilteredAsyncIterator(self, predicate)
async def flatten(self): async def flatten(self) -> List[T]:
ret = [] return [element async for element in self]
while True:
try:
item = await self.next()
except NoMoreItems:
return ret
else:
ret.append(item)
def __aiter__(self): async def __anext__(self) -> T:
return self
async def __anext__(self):
try: try:
msg = await self.next() return await self.next()
except NoMoreItems: except NoMoreItems:
raise StopAsyncIteration() raise StopAsyncIteration()
else:
return msg
def _identity(x): def _identity(x):
return x return x
class _ChunkedAsyncIterator(_AsyncIterator): class _ChunkedAsyncIterator(_AsyncIterator[T]):
def __init__(self, iterator, max_size): def __init__(self, iterator, max_size):
self.iterator = iterator self.iterator = iterator
self.max_size = max_size self.max_size = max_size
async def next(self): async def next(self) -> T:
ret = [] ret = []
n = 0 n = 0
while n < self.max_size: while n < self.max_size:
@ -117,17 +126,17 @@ class _ChunkedAsyncIterator(_AsyncIterator):
n += 1 n += 1
return ret return ret
class _MappedAsyncIterator(_AsyncIterator): class _MappedAsyncIterator(_AsyncIterator[T]):
def __init__(self, iterator, func): def __init__(self, iterator, func):
self.iterator = iterator self.iterator = iterator
self.func = func self.func = func
async def next(self): async def next(self) -> T:
# this raises NoMoreItems and will propagate appropriately # this raises NoMoreItems and will propagate appropriately
item = await self.iterator.next() item = await self.iterator.next()
return await maybe_coroutine(self.func, item) return await maybe_coroutine(self.func, item)
class _FilteredAsyncIterator(_AsyncIterator): class _FilteredAsyncIterator(_AsyncIterator[T]):
def __init__(self, iterator, predicate): def __init__(self, iterator, predicate):
self.iterator = iterator self.iterator = iterator
@ -136,7 +145,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
self.predicate = predicate self.predicate = predicate
async def next(self): async def next(self) -> T:
getter = self.iterator.next getter = self.iterator.next
pred = self.predicate pred = self.predicate
while True: while True:
@ -146,7 +155,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
if ret: if ret:
return item return item
class ReactionIterator(_AsyncIterator): class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
def __init__(self, message, emoji, limit=100, after=None): def __init__(self, message, emoji, limit=100, after=None):
self.message = message self.message = message
self.limit = limit self.limit = limit
@ -159,7 +168,7 @@ class ReactionIterator(_AsyncIterator):
self.channel_id = message.channel.id self.channel_id = message.channel.id
self.users = asyncio.Queue() self.users = asyncio.Queue()
async def next(self): async def next(self) -> T:
if self.users.empty(): if self.users.empty():
await self.fill_users() await self.fill_users()
@ -194,7 +203,7 @@ class ReactionIterator(_AsyncIterator):
else: else:
await self.users.put(User(state=self.state, data=element)) await self.users.put(User(state=self.state, data=element))
class HistoryIterator(_AsyncIterator): class HistoryIterator(_AsyncIterator['Message']):
"""Iterator for receiving a channel's message history. """Iterator for receiving a channel's message history.
The messages endpoint has two behaviours we care about here: The messages endpoint has two behaviours we care about here:
@ -280,7 +289,7 @@ class HistoryIterator(_AsyncIterator):
if (self.after and self.after != OLDEST_OBJECT): if (self.after and self.after != OLDEST_OBJECT):
self._filter = lambda m: int(m['id']) > self.after.id self._filter = lambda m: int(m['id']) > self.after.id
async def next(self): async def next(self) -> T:
if self.messages.empty(): if self.messages.empty():
await self.fill_messages() await self.fill_messages()
@ -298,26 +307,6 @@ class HistoryIterator(_AsyncIterator):
self.retrieve = r self.retrieve = r
return r > 0 return r > 0
async def flatten(self):
# this is similar to fill_messages except it uses a list instead
# of a queue to place the messages in.
result = []
channel = await self.messageable._get_channel()
self.channel = channel
while self._get_retrieve():
data = await self._retrieve_messages(self.retrieve)
if len(data) < 100:
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
if self._filter:
data = filter(self._filter, data)
for element in data:
result.append(self.state.create_message(channel=channel, data=element))
return result
async def fill_messages(self): async def fill_messages(self):
if not hasattr(self, 'channel'): if not hasattr(self, 'channel'):
# do the required set up # do the required set up
@ -371,7 +360,7 @@ class HistoryIterator(_AsyncIterator):
return data return data
return [] return []
class AuditLogIterator(_AsyncIterator): class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None): def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
if isinstance(before, datetime.datetime): if isinstance(before, datetime.datetime):
before = Object(id=time_snowflake(before, high=False)) before = Object(id=time_snowflake(before, high=False))
@ -433,7 +422,7 @@ class AuditLogIterator(_AsyncIterator):
self.after = Object(id=int(entries[0]['id'])) self.after = Object(id=int(entries[0]['id']))
return data.get('users', []), entries return data.get('users', []), entries
async def next(self): async def next(self) -> T:
if self.entries.empty(): if self.entries.empty():
await self._fill() await self._fill()
@ -476,7 +465,7 @@ class AuditLogIterator(_AsyncIterator):
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
class GuildIterator(_AsyncIterator): class GuildIterator(_AsyncIterator['Guild']):
"""Iterator for receiving the client's guilds. """Iterator for receiving the client's guilds.
The guilds endpoint has the same two behaviours as described The guilds endpoint has the same two behaviours as described
@ -530,7 +519,7 @@ class GuildIterator(_AsyncIterator):
else: else:
self._retrieve_guilds = self._retrieve_guilds_before_strategy self._retrieve_guilds = self._retrieve_guilds_before_strategy
async def next(self): async def next(self) -> T:
if self.guilds.empty(): if self.guilds.empty():
await self.fill_guilds() await self.fill_guilds()
@ -552,20 +541,6 @@ class GuildIterator(_AsyncIterator):
from .guild import Guild from .guild import Guild
return Guild(state=self.state, data=data) return Guild(state=self.state, data=data)
async def flatten(self):
result = []
while self._get_retrieve():
data = await self._retrieve_guilds(self.retrieve)
if len(data) < 100:
self.limit = 0
if self._filter:
data = filter(self._filter, data)
for element in data:
result.append(self.create_guild(element))
return result
async def fill_guilds(self): async def fill_guilds(self):
if self._get_retrieve(): if self._get_retrieve():
data = await self._retrieve_guilds(self.retrieve) data = await self._retrieve_guilds(self.retrieve)
@ -602,7 +577,7 @@ class GuildIterator(_AsyncIterator):
self.after = Object(id=int(data[0]['id'])) self.after = Object(id=int(data[0]['id']))
return data return data
class MemberIterator(_AsyncIterator): class MemberIterator(_AsyncIterator['Member']):
def __init__(self, guild, limit=1000, after=None): def __init__(self, guild, limit=1000, after=None):
if isinstance(after, datetime.datetime): if isinstance(after, datetime.datetime):
@ -616,7 +591,7 @@ class MemberIterator(_AsyncIterator):
self.get_members = self.state.http.get_members self.get_members = self.state.http.get_members
self.members = asyncio.Queue() self.members = asyncio.Queue()
async def next(self): async def next(self) -> T:
if self.members.empty(): if self.members.empty():
await self.fill_members() await self.fill_members()

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -41,6 +39,11 @@ from .enums import Status, try_enum
from .colour import Colour from .colour import Colour
from .object import Object from .object import Object
__all__ = (
'VoiceState',
'Member',
)
class VoiceState: class VoiceState:
"""Represents a Discord user's voice state. """Represents a Discord user's voice state.
@ -69,7 +72,7 @@ class VoiceState:
.. versionadded:: 1.7 .. versionadded:: 1.7
requested_to_speak_at: Optional[:class:`datetime.datetime`] requested_to_speak_at: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC that the member An aware datetime object that specifies the date and time in UTC that the member
requested to speak. It will be ``None`` if they are not requesting to speak requested to speak. It will be ``None`` if they are not requesting to speak
anymore or have been accepted to speak. anymore or have been accepted to speak.
@ -113,7 +116,8 @@ class VoiceState:
('requested_to_speak_at', self.requested_to_speak_at), ('requested_to_speak_at', self.requested_to_speak_at),
('channel', self.channel) ('channel', self.channel)
] ]
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs)) inner = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {inner}>'
def flatten_user(cls): def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
@ -129,7 +133,7 @@ def flatten_user(cls):
# slotted members are implemented as member_descriptors in Type.__dict__ # slotted members are implemented as member_descriptors in Type.__dict__
if not hasattr(value, '__annotations__'): if not hasattr(value, '__annotations__'):
getter = attrgetter('_user.' + attr) getter = attrgetter('_user.' + attr)
setattr(cls, attr, property(getter, doc='Equivalent to :attr:`User.%s`' % attr)) setattr(cls, attr, property(getter, doc=f'Equivalent to :attr:`User.{attr}`'))
else: else:
# Technically, this can also use attrgetter # Technically, this can also use attrgetter
# However I'm not sure how I feel about "functions" returning properties # However I'm not sure how I feel about "functions" returning properties
@ -184,7 +188,7 @@ class Member(discord.abc.Messageable, _BaseUser):
Attributes Attributes
---------- ----------
joined_at: Optional[:class:`datetime.datetime`] joined_at: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC that the member joined the guild. An aware datetime object that specifies the date and time in UTC that the member joined the guild.
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``. 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`]] activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
The activities that the user is currently doing. The activities that the user is currently doing.
@ -204,7 +208,7 @@ class Member(discord.abc.Messageable, _BaseUser):
.. versionadded:: 1.6 .. versionadded:: 1.6
premium_since: Optional[:class:`datetime.datetime`] premium_since: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC when the member used their An aware datetime object that specifies the date and time in UTC when the member used their
Nitro boost on the guild, if available. This could be ``None``. Nitro boost on the guild, if available. This could be ``None``.
""" """
@ -232,8 +236,8 @@ class Member(discord.abc.Messageable, _BaseUser):
return self.id return self.id
def __repr__(self): def __repr__(self):
return '<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}' \ return f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}' \
' bot={1.bot} nick={0.nick!r} guild={0.guild!r}>'.format(self, self._user) f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, _BaseUser) and other.id == self.id return isinstance(other, _BaseUser) and other.id == self.id
@ -432,8 +436,8 @@ class Member(discord.abc.Messageable, _BaseUser):
def mention(self): def mention(self):
""":class:`str`: Returns a string that allows you to mention the member.""" """:class:`str`: Returns a string that allows you to mention the member."""
if self.nick: if self.nick:
return '<@!%s>' % self.id return f'<@!{self._user.id}>'
return '<@%s>' % self.id return f'<@{self._user.id}>'
@property @property
def display_name(self): def display_name(self):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
__all__ = (
'AllowedMentions',
)
class _FakeBool: class _FakeBool:
def __repr__(self): def __repr__(self):
return 'True' return 'True'

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -33,7 +31,6 @@ from . import utils
from .reaction import Reaction from .reaction import Reaction
from .emoji import Emoji from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .calls import CallMessage
from .enums import MessageType, ChannelType, try_enum from .enums import MessageType, ChannelType, try_enum
from .errors import InvalidArgument, ClientException, HTTPException from .errors import InvalidArgument, ClientException, HTTPException
from .embeds import Embed from .embeds import Embed
@ -58,7 +55,7 @@ def convert_emoji_reaction(emoji):
emoji = emoji.emoji emoji = emoji.emoji
if isinstance(emoji, Emoji): if isinstance(emoji, Emoji):
return '%s:%s' % (emoji.name, emoji.id) return f'{emoji.name}:{emoji.id}'
if isinstance(emoji, PartialEmoji): if isinstance(emoji, PartialEmoji):
return emoji._as_reaction() return emoji._as_reaction()
if isinstance(emoji, str): if isinstance(emoji, str):
@ -66,7 +63,7 @@ def convert_emoji_reaction(emoji):
# No existing emojis have <> in them, so this should be okay. # No existing emojis have <> in them, so this should be okay.
return emoji.strip('<>') return emoji.strip('<>')
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji)) raise InvalidArgument(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.')
class Attachment(Hashable): class Attachment(Hashable):
"""Represents an attachment from Discord. """Represents an attachment from Discord.
@ -477,12 +474,6 @@ class Message(Hashable):
channel: Union[:class:`abc.Messageable`] channel: Union[:class:`abc.Messageable`]
The :class:`TextChannel` that the message was sent from. The :class:`TextChannel` that the message was sent from.
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
call: Optional[:class:`CallMessage`]
The call that the message refers to. This is only applicable to messages of type
:attr:`MessageType.call`.
.. deprecated:: 1.7
reference: Optional[:class:`~discord.MessageReference`] reference: Optional[:class:`~discord.MessageReference`]
The message that this message references. This is only applicable to messages of The message that this message references. This is only applicable to messages of
type :attr:`MessageType.pins_add`, crossposted messages created by a type :attr:`MessageType.pins_add`, crossposted messages created by a
@ -558,7 +549,7 @@ class Message(Hashable):
'mention_everyone', 'embeds', 'id', 'mentions', 'author', 'mention_everyone', 'embeds', 'id', 'mentions', 'author',
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments', '_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned', '_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags', 'role_mentions', '_cs_raw_role_mentions', 'type', 'flags',
'_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference', '_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference',
'application', 'activity', 'stickers') 'application', 'activity', 'stickers')
@ -572,7 +563,6 @@ class Message(Hashable):
self.application = data.get('application') self.application = data.get('application')
self.activity = data.get('activity') self.activity = data.get('activity')
self.channel = channel self.channel = channel
self.call = None
self._edited_timestamp = utils.parse_time(data['edited_timestamp']) self._edited_timestamp = utils.parse_time(data['edited_timestamp'])
self.type = try_enum(MessageType, data['type']) self.type = try_enum(MessageType, data['type'])
self.pinned = data['pinned'] self.pinned = data['pinned']
@ -605,9 +595,9 @@ class Message(Hashable):
ref.resolved = self.__class__(channel=chan, data=resolved, state=state) ref.resolved = self.__class__(channel=chan, data=resolved, state=state)
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'): for handler in ('author', 'member', 'mentions', 'mention_roles', 'flags'):
try: try:
getattr(self, '_handle_%s' % handler)(data[handler]) getattr(self, f'_handle_{handler}')(data[handler])
except KeyError: except KeyError:
continue continue
@ -779,26 +769,6 @@ class Message(Hashable):
if role is not None: if role is not None:
self.role_mentions.append(role) self.role_mentions.append(role)
def _handle_call(self, call):
if call is None or self.type is not MessageType.call:
self.call = None
return
# we get the participant source from the mentions array or
# the author
participants = []
for uid in map(int, call.get('participants', [])):
if uid == self.author.id:
participants.append(self.author)
else:
user = utils.find(lambda u: u.id == uid, self.mentions)
if user is not None:
participants.append(user)
call['participants'] = participants
self.call = CallMessage(message=self, **call)
def _rebind_channel_reference(self, new_channel): def _rebind_channel_reference(self, new_channel):
self.channel = new_channel self.channel = new_channel
@ -856,23 +826,23 @@ class Message(Hashable):
.. note:: .. note::
This *does not* affect markdown. If you want to escape This *does not* affect markdown. If you want to escape
or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown` or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown`
respectively, along with this function. respectively, along with this function.
""" """
transformations = { transformations = {
re.escape('<#%s>' % channel.id): '#' + channel.name re.escape(f'<#{channel.id}>'): '#' + channel.name
for channel in self.channel_mentions for channel in self.channel_mentions
} }
mention_transforms = { mention_transforms = {
re.escape('<@%s>' % member.id): '@' + member.display_name re.escape(f'<@{member.id}>'): '@' + member.display_name
for member in self.mentions for member in self.mentions
} }
# add the <@!user_id> cases as well.. # add the <@!user_id> cases as well..
second_mention_transforms = { second_mention_transforms = {
re.escape('<@!%s>' % member.id): '@' + member.display_name re.escape(f'<@!{member.id}>'): '@' + member.display_name
for member in self.mentions for member in self.mentions
} }
@ -881,7 +851,7 @@ class Message(Hashable):
if self.guild is not None: if self.guild is not None:
role_transforms = { role_transforms = {
re.escape('<@&%s>' % role.id): '@' + role.name re.escape(f'<@&{role.id}>'): '@' + role.name
for role in self.role_mentions for role in self.role_mentions
} }
transformations.update(role_transforms) transformations.update(role_transforms)
@ -900,7 +870,7 @@ class Message(Hashable):
@property @property
def edited_at(self): def edited_at(self):
"""Optional[:class:`datetime.datetime`]: A naive UTC datetime object containing the edited time of the message.""" """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message."""
return self._edited_timestamp return self._edited_timestamp
@property @property
@ -930,7 +900,7 @@ class Message(Hashable):
return self.content return self.content
if self.type is MessageType.pins_add: if self.type is MessageType.pins_add:
return '{0.name} pinned a message to this channel.'.format(self.author) return f'{self.author.name} pinned a message to this channel.'
if self.type is MessageType.recipient_add: if self.type is MessageType.recipient_add:
return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0]) return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0])
@ -942,7 +912,7 @@ class Message(Hashable):
return '{0.author.name} changed the channel name: {0.content}'.format(self) return '{0.author.name} changed the channel name: {0.content}'.format(self)
if self.type is MessageType.channel_icon_change: if self.type is MessageType.channel_icon_change:
return '{0.author.name} changed the channel icon.'.format(self) return f'{self.author.name} changed the channel icon.'
if self.type is MessageType.new_member: if self.type is MessageType.new_member:
formats = [ formats = [
@ -961,27 +931,11 @@ class Message(Hashable):
"Yay you made it, {0}!", "Yay you made it, {0}!",
] ]
# manually reconstruct the epoch with millisecond precision, because created_at_ms = int(self.created_at.timestamp() * 1000)
# datetime.datetime.timestamp() doesn't return the exact posix
# timestamp with the precision that we need
created_at_ms = int((self.created_at - datetime.datetime(1970, 1, 1)).total_seconds() * 1000)
return formats[created_at_ms % len(formats)].format(self.author.name) return formats[created_at_ms % len(formats)].format(self.author.name)
if self.type is MessageType.call:
# we're at the call message type now, which is a bit more complicated.
# we can make the assumption that Message.channel is a PrivateChannel
# with the type ChannelType.group or ChannelType.private
call_ended = self.call.ended_timestamp is not None
if self.channel.me in self.call.participants:
return '{0.author.name} started a call.'.format(self)
elif call_ended:
return 'You missed a call from {0.author.name}'.format(self)
else:
return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self)
if self.type is MessageType.premium_guild_subscription: if self.type is MessageType.premium_guild_subscription:
return '{0.author.name} just boosted the server!'.format(self) return f'{self.author.name} just boosted the server!'
if self.type is MessageType.premium_guild_tier_1: if self.type is MessageType.premium_guild_tier_1:
return '{0.author.name} just boosted the server! {0.guild} has achieved **Level 1!**'.format(self) return '{0.author.name} just boosted the server! {0.guild} has achieved **Level 1!**'.format(self)
@ -1050,7 +1004,7 @@ class Message(Hashable):
except HTTPException: except HTTPException:
pass pass
asyncio.ensure_future(delete(), loop=self._state.loop) asyncio.create_task(delete())
else: else:
try: try:
await self._state.http.delete_message(self.channel.id, self.id) await self._state.http.delete_message(self.channel.id, self.id)
@ -1344,29 +1298,6 @@ class Message(Hashable):
""" """
await self._state.http.clear_reactions(self.channel.id, self.id) await self._state.http.clear_reactions(self.channel.id, self.id)
@utils.deprecated()
async def ack(self):
"""|coro|
Marks this message as read.
The user must not be a bot user.
.. deprecated:: 1.7
Raises
-------
HTTPException
Acking failed.
ClientException
You must not be a bot user.
"""
state = self._state
if state.is_bot:
raise ClientException('Must not be a bot account to ack messages.')
return await state.http.ack_message(self.channel.id, self.id)
async def reply(self, content=None, **kwargs): async def reply(self, content=None, **kwargs):
"""|coro| """|coro|
@ -1486,7 +1417,7 @@ class PartialMessage(Hashable):
def __init__(self, *, channel, id): def __init__(self, *, channel, id):
if channel.type not in (ChannelType.text, ChannelType.news, ChannelType.private): if channel.type not in (ChannelType.text, ChannelType.news, ChannelType.private):
raise TypeError('Expected TextChannel or DMChannel not %r' % type(channel)) raise TypeError(f'Expected TextChannel or DMChannel not {type(channel)!r}')
self.channel = channel self.channel = channel
self._state = channel._state self._state = channel._state

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,6 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
__all__ = (
'EqualityComparable',
'Hashable',
)
class EqualityComparable: class EqualityComparable:
__slots__ = () __slots__ = ()

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
from . import utils from . import utils
from .mixins import Hashable from .mixins import Hashable
__all__ = (
'Object',
)
class Object(Hashable): class Object(Hashable):
"""Represents a generic Discord object. """Represents a generic Discord object.
@ -65,12 +67,12 @@ class Object(Hashable):
try: try:
id = int(id) id = int(id)
except ValueError: except ValueError:
raise TypeError('id parameter must be convertable to int not {0.__class__!r}'.format(id)) from None raise TypeError(f'id parameter must be convertable to int not {id.__class__!r}') from None
else: else:
self.id = id self.id = id
def __repr__(self): def __repr__(self):
return '<Object id=%r>' % self.id return f'<Object id={self.id!r}>'
@property @property
def created_at(self): def created_at(self):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -28,6 +26,12 @@ import struct
from .errors import DiscordException from .errors import DiscordException
__all__ = (
'OggError',
'OggPage',
'OggStream',
)
class OggError(DiscordException): class OggError(DiscordException):
"""An exception that is thrown for Ogg stream parsing errors.""" """An exception that is thrown for Ogg stream parsing errors."""
pass pass

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -35,6 +33,12 @@ import sys
from .errors import DiscordException from .errors import DiscordException
__all__ = (
'Encoder',
'OpusError',
'OpusNotLoaded',
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
c_int_ptr = ctypes.POINTER(ctypes.c_int) c_int_ptr = ctypes.POINTER(ctypes.c_int)
@ -185,7 +189,7 @@ def _load_default():
_basedir = os.path.dirname(os.path.abspath(__file__)) _basedir = os.path.dirname(os.path.abspath(__file__))
_bitness = struct.calcsize('P') * 8 _bitness = struct.calcsize('P') * 8
_target = 'x64' if _bitness > 32 else 'x86' _target = 'x64' if _bitness > 32 else 'x86'
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target)) _filename = os.path.join(_basedir, 'bin', f'libopus-0.{_target}.dll')
_lib = libopus_loader(_filename) _lib = libopus_loader(_filename)
else: else:
_lib = libopus_loader(ctypes.util.find_library('opus')) _lib = libopus_loader(ctypes.util.find_library('opus'))
@ -310,14 +314,14 @@ class Encoder(_OpusStruct):
def set_bandwidth(self, req): def set_bandwidth(self, req):
if req not in band_ctl: if req not in band_ctl:
raise KeyError('%r is not a valid bandwidth setting. Try one of: %s' % (req, ','.join(band_ctl))) raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}')
k = band_ctl[req] k = band_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k) _lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
def set_signal_type(self, req): def set_signal_type(self, req):
if req not in signal_ctl: if req not in signal_ctl:
raise KeyError('%r is not a valid signal setting. Try one of: %s' % (req, ','.join(signal_ctl))) raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
k = signal_ctl[req] k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k) _lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,9 @@ DEALINGS IN THE SOFTWARE.
from .asset import Asset from .asset import Asset
from . import utils from . import utils
__all__ = (
'PartialEmoji',
)
class _EmojiTag: class _EmojiTag:
__slots__ = () __slots__ = ()
@ -103,8 +104,8 @@ class PartialEmoji(_EmojiTag):
if self.id is None: if self.id is None:
return self.name return self.name
if self.animated: if self.animated:
return '<a:%s:%s>' % (self.name, self.id) return f'<a:{self.name}:{self.id}>'
return '<:%s:%s>' % (self.name, self.id) return f'<:{self.name}:{self.id}>'
def __repr__(self): def __repr__(self):
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self) return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
@ -134,7 +135,7 @@ class PartialEmoji(_EmojiTag):
def _as_reaction(self): def _as_reaction(self):
if self.id is None: if self.id is None:
return self.name return self.name
return '%s:%s' % (self.name, self.id) return f'{self.name}:{self.id}'
@property @property
def created_at(self): def created_at(self):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -96,12 +94,12 @@ class Permissions(BaseFlags):
def __init__(self, permissions=0, **kwargs): def __init__(self, permissions=0, **kwargs):
if not isinstance(permissions, int): if not isinstance(permissions, int):
raise TypeError('Expected int parameter, received %s instead.' % permissions.__class__.__name__) raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
self.value = permissions self.value = permissions
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_FLAGS: if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid permission name.' % key) raise TypeError(f'{key!r} is not a valid permission name.')
setattr(self, key, value) setattr(self, key, value)
def is_subset(self, other): def is_subset(self, other):
@ -109,14 +107,14 @@ class Permissions(BaseFlags):
if isinstance(other, Permissions): if isinstance(other, Permissions):
return (self.value & other.value) == self.value return (self.value & other.value) == self.value
else: else:
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__)) raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
def is_superset(self, other): def is_superset(self, other):
"""Returns ``True`` if self has the same or more permissions as other.""" """Returns ``True`` if self has the same or more permissions as other."""
if isinstance(other, Permissions): if isinstance(other, Permissions):
return (self.value | other.value) == self.value return (self.value | other.value) == self.value
else: else:
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__)) raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
def is_strict_subset(self, other): def is_strict_subset(self, other):
"""Returns ``True`` if the permissions on other are a strict subset of those on self.""" """Returns ``True`` if the permissions on other are a strict subset of those on self."""
@ -541,7 +539,7 @@ class PermissionOverwrite:
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_NAMES: if key not in self.VALID_NAMES:
raise ValueError('no permission called {0}.'.format(key)) raise ValueError(f'no permission called {key}.')
setattr(self, key, value) setattr(self, key, value)
@ -550,7 +548,7 @@ class PermissionOverwrite:
def _set(self, key, value): def _set(self, key, value):
if value not in (True, None, False): if value not in (True, None, False):
raise TypeError('Expected bool or NoneType, received {0.__class__.__name__}'.format(value)) raise TypeError(f'Expected bool or NoneType, received {value.__class__.__name__}')
if value is None: if value is None:
self._values.pop(key, None) self._values.pop(key, None)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -313,7 +311,7 @@ class FFmpegOpusAudio(FFmpegAudio):
'-c:a', codec, '-c:a', codec,
'-ar', '48000', '-ar', '48000',
'-ac', '2', '-ac', '2',
'-b:a', '%sk' % bitrate, '-b:a', f'{bitrate}k',
'-loglevel', 'warning')) '-loglevel', 'warning'))
if isinstance(options, str): if isinstance(options, str):
@ -421,7 +419,7 @@ class FFmpegOpusAudio(FFmpegAudio):
if isinstance(method, str): if isinstance(method, str):
probefunc = getattr(cls, '_probe_codec_' + method, None) probefunc = getattr(cls, '_probe_codec_' + method, None)
if probefunc is None: if probefunc is None:
raise AttributeError("Invalid probe method '%s'" % method) raise AttributeError(f"Invalid probe method {method!r}")
if probefunc is cls._probe_codec_native: if probefunc is cls._probe_codec_native:
fallback = cls._probe_codec_fallback fallback = cls._probe_codec_fallback
@ -431,7 +429,7 @@ class FFmpegOpusAudio(FFmpegAudio):
fallback = cls._probe_codec_fallback fallback = cls._probe_codec_fallback
else: else:
raise TypeError("Expected str or callable for parameter 'probe', " \ raise TypeError("Expected str or callable for parameter 'probe', " \
"not '{0.__class__.__name__}'" .format(method)) f"not '{method.__class__.__name__}'")
codec = bitrate = None codec = bitrate = None
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -519,7 +517,7 @@ class PCMVolumeTransformer(AudioSource):
def __init__(self, original, volume=1.0): def __init__(self, original, volume=1.0):
if not isinstance(original, AudioSource): if not isinstance(original, AudioSource):
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(original)) raise TypeError(f'expected AudioSource not {original.__class__.__name__}.')
if original.is_opus(): if original.is_opus():
raise ClientException('AudioSource must not be Opus encoded.') raise ClientException('AudioSource must not be Opus encoded.')
@ -619,7 +617,7 @@ class AudioPlayer(threading.Thread):
exc.__context__ = error exc.__context__ = error
traceback.print_exception(type(exc), exc, exc.__traceback__) traceback.print_exception(type(exc), exc, exc.__traceback__)
elif error: elif error:
msg = 'Exception in voice thread {}'.format(self.name) msg = f'Exception in voice thread {self.name}'
log.exception(msg, exc_info=error) log.exception(msg, exc_info=error)
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
traceback.print_exception(type(error), error, error.__traceback__) traceback.print_exception(type(error), error, error.__traceback__)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,10 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
__all__ = (
'RawMessageDeleteEvent',
'RawBulkMessageDeleteEvent',
'RawMessageUpdateEvent',
'RawReactionActionEvent',
'RawReactionClearEvent',
'RawReactionClearEmojiEvent',
)
class _RawReprMixin: class _RawReprMixin:
def __repr__(self): def __repr__(self):
value = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in self.__slots__) value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
return '<%s %s>' % (self.__class__.__name__, value) return f'<{self.__class__.__name__} {value}>'
class RawMessageDeleteEvent(_RawReprMixin): class RawMessageDeleteEvent(_RawReprMixin):
"""Represents the event payload for a :func:`on_raw_message_delete` event. """Represents the event payload for a :func:`on_raw_message_delete` event.

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
from .iterators import ReactionIterator from .iterators import ReactionIterator
__all__ = (
'Reaction',
)
class Reaction: class Reaction:
"""Represents a reaction to a message. """Represents a reaction to a message.

View File

@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .enums import RelationshipType, try_enum
from . import utils
class Relationship:
"""Represents a relationship in Discord.
A relationship is like a friendship, a person who is blocked, etc.
Only non-bot accounts can have relationships.
.. deprecated:: 1.7
Attributes
-----------
user: :class:`User`
The user you have the relationship with.
type: :class:`RelationshipType`
The type of relationship you have.
"""
__slots__ = ('type', 'user', '_state')
def __init__(self, *, state, data):
self._state = state
self.type = try_enum(RelationshipType, data['type'])
self.user = state.store_user(data['user'])
def __repr__(self):
return '<Relationship user={0.user!r} type={0.type!r}>'.format(self)
@utils.deprecated()
async def delete(self):
"""|coro|
Deletes the relationship.
.. deprecated:: 1.7
Raises
------
HTTPException
Deleting the relationship failed.
"""
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
Accepting the relationship failed.
"""
await self._state.http.add_relationship(self.user.id)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -30,6 +28,11 @@ from .colour import Colour
from .mixins import Hashable from .mixins import Hashable
from .utils import snowflake_time, _get_as_snowflake from .utils import snowflake_time, _get_as_snowflake
__all__ = (
'RoleTags',
'Role',
)
class RoleTags: class RoleTags:
"""Represents tags on a role. """Represents tags on a role.
@ -251,7 +254,7 @@ class Role(Hashable):
@property @property
def mention(self): def mention(self):
""":class:`str`: Returns a string that allows you to mention a role.""" """:class:`str`: Returns a string that allows you to mention a role."""
return '<@&%s>' % self.id return f'<@&{self.id}>'
@property @property
def members(self): def members(self):

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -46,6 +44,11 @@ from .errors import (
from . import utils from . import utils
from .enums import Status from .enums import Status
__all__ = (
'AutoShardedClient',
'ShardInfo',
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class EventType: class EventType:
@ -319,7 +322,7 @@ class AutoShardedClient(Client):
def _get_state(self, **options): def _get_state(self, **options):
return AutoShardedConnectionState(dispatch=self.dispatch, return AutoShardedConnectionState(dispatch=self.dispatch,
handlers=self._handlers, syncer=self._syncer, handlers=self._handlers,
hooks=self._hooks, http=self.http, loop=self.loop, **options) hooks=self._hooks, http=self.http, loop=self.loop, **options)
@property @property

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -44,7 +42,6 @@ from .emoji import Emoji
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .message import Message from .message import Message
from .relationship import Relationship
from .channel import * from .channel import *
from .raw_models import * from .raw_models import *
from .member import Member from .member import Member
@ -54,6 +51,7 @@ from . import utils
from .flags import Intents, MemberCacheFlags from .flags import Intents, MemberCacheFlags
from .object import Object from .object import Object
from .invite import Invite from .invite import Invite
from .interactions import Interaction
class ChunkRequest: class ChunkRequest:
def __init__(self, guild_id, loop, resolver, *, cache=True): def __init__(self, guild_id, loop, resolver, *, cache=True):
@ -104,7 +102,7 @@ async def logging_coroutine(coroutine, *, info):
log.exception('Exception occurred during %s', info) log.exception('Exception occurred during %s', info)
class ConnectionState: class ConnectionState:
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options): def __init__(self, *, dispatch, handlers, hooks, http, loop, **options):
self.loop = loop self.loop = loop
self.http = http self.http = http
self.max_messages = options.get('max_messages', 1000) self.max_messages = options.get('max_messages', 1000)
@ -112,12 +110,11 @@ class ConnectionState:
self.max_messages = 1000 self.max_messages = 1000
self.dispatch = dispatch self.dispatch = dispatch
self.syncer = syncer
self.is_bot = None
self.handlers = handlers self.handlers = handlers
self.hooks = hooks self.hooks = hooks
self.shard_count = None self.shard_count = None
self._ready_task = None self._ready_task = None
self.application_id = utils._get_as_snowflake(options, 'application_id')
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0) self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0) self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
if self.guild_ready_timeout < 0: if self.guild_ready_timeout < 0:
@ -149,7 +146,7 @@ class ConnectionState:
intents = options.get('intents', None) intents = options.get('intents', None)
if intents is not None: if intents is not None:
if not isinstance(intents, Intents): if not isinstance(intents, Intents):
raise TypeError('intents parameter must be Intent not %r' % type(intents)) raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
else: else:
intents = Intents.default() intents = Intents.default()
@ -175,7 +172,7 @@ class ConnectionState:
cache_flags = MemberCacheFlags.from_intents(intents) cache_flags = MemberCacheFlags.from_intents(intents)
else: else:
if not isinstance(cache_flags, MemberCacheFlags): if not isinstance(cache_flags, MemberCacheFlags):
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags)) raise TypeError(f'member_cache_flags parameter must be MemberCacheFlags not {type(cache_flags)!r}')
cache_flags._verify_intents(intents) cache_flags._verify_intents(intents)
@ -198,7 +195,6 @@ class ConnectionState:
self.user = None self.user = None
self._users = weakref.WeakValueDictionary() self._users = weakref.WeakValueDictionary()
self._emojis = {} self._emojis = {}
self._calls = {}
self._guilds = {} self._guilds = {}
self._voice_clients = {} self._voice_clients = {}
@ -340,7 +336,7 @@ class ConnectionState:
channel_id = channel.id channel_id = channel.id
self._private_channels[channel_id] = channel self._private_channels[channel_id] = channel
if self.is_bot and len(self._private_channels) > 128: if len(self._private_channels) > 128:
_, to_remove = self._private_channels.popitem(last=False) _, to_remove = self._private_channels.popitem(last=False)
if isinstance(to_remove, DMChannel): if isinstance(to_remove, DMChannel):
self._private_channels_by_user.pop(to_remove.recipient.id, None) self._private_channels_by_user.pop(to_remove.recipient.id, None)
@ -405,36 +401,34 @@ class ConnectionState:
async def _delay_ready(self): async def _delay_ready(self):
try: try:
# only real bots wait for GUILD_CREATE streaming states = []
if self.is_bot: while True:
states = [] # this snippet of code is basically waiting N seconds
while True: # until the last GUILD_CREATE was sent
# this snippet of code is basically waiting N seconds try:
# until the last GUILD_CREATE was sent guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout)
try: except asyncio.TimeoutError:
guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout) break
except asyncio.TimeoutError: else:
break if self._guild_needs_chunking(guild):
future = await self.chunk_guild(guild, wait=False)
states.append((guild, future))
else: else:
if self._guild_needs_chunking(guild): if guild.unavailable is False:
future = await self.chunk_guild(guild, wait=False) self.dispatch('guild_available', guild)
states.append((guild, future))
else: else:
if guild.unavailable is False: self.dispatch('guild_join', guild)
self.dispatch('guild_available', guild)
else:
self.dispatch('guild_join', guild)
for guild, future in states: for guild, future in states:
try: try:
await asyncio.wait_for(future, timeout=5.0) await asyncio.wait_for(future, timeout=5.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id) log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
if guild.unavailable is False: if guild.unavailable is False:
self.dispatch('guild_available', guild) self.dispatch('guild_available', guild)
else: else:
self.dispatch('guild_join', guild) self.dispatch('guild_join', guild)
# remove the state # remove the state
try: try:
@ -442,10 +436,6 @@ class ConnectionState:
except AttributeError: except AttributeError:
pass # already been deleted somehow pass # already been deleted somehow
# call GUILD_SYNC after we're done chunking
if not self.is_bot:
log.info('Requesting GUILD_SYNC for %s guilds', len(self.guilds))
await self.syncer([s.id for s in self.guilds])
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
else: else:
@ -464,23 +454,19 @@ class ConnectionState:
self.user = user = ClientUser(state=self, data=data['user']) self.user = user = ClientUser(state=self, data=data['user'])
self._users[user.id] = user self._users[user.id] = user
if self.application_id is None:
try:
application = data['application']
except KeyError:
pass
else:
self.application_id = utils._get_as_snowflake(application, 'id')
for guild_data in data['guilds']: for guild_data in data['guilds']:
self._add_guild_from_data(guild_data) self._add_guild_from_data(guild_data)
for relationship in data.get('relationships', []):
try:
r_id = int(relationship['id'])
except KeyError:
continue
else:
user._relationships[r_id] = Relationship(state=self, data=relationship)
for pm in data.get('private_channels', []):
factory, _ = _channel_factory(pm['type'])
self._add_private_channel(factory(me=user, data=pm, state=self))
self.dispatch('connect') self.dispatch('connect')
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop) self._ready_task = asyncio.create_task(self._delay_ready())
def parse_resumed(self, data): def parse_resumed(self, data):
self.dispatch('resumed') self.dispatch('resumed')
@ -601,6 +587,10 @@ class ConnectionState:
if reaction: if reaction:
self.dispatch('reaction_clear_emoji', reaction) self.dispatch('reaction_clear_emoji', reaction)
def parse_interaction_create(self, data):
interaction = Interaction(data=data, state=self)
self.dispatch('interaction', interaction)
def parse_presence_update(self, data): def parse_presence_update(self, data):
guild_id = utils._get_as_snowflake(data, 'guild_id') guild_id = utils._get_as_snowflake(data, 'guild_id')
guild = self._get_guild(guild_id) guild = self._get_guild(guild_id)
@ -724,22 +714,6 @@ class ConnectionState:
else: else:
self.dispatch('guild_channel_pins_update', channel, last_pin) self.dispatch('guild_channel_pins_update', channel, last_pin)
def parse_channel_recipient_add(self, data):
channel = self._get_private_channel(int(data['channel_id']))
user = self.store_user(data['user'])
channel.recipients.append(user)
self.dispatch('group_join', channel, user)
def parse_channel_recipient_remove(self, data):
channel = self._get_private_channel(int(data['channel_id']))
user = self.store_user(data['user'])
try:
channel.recipients.remove(user)
except ValueError:
pass
else:
self.dispatch('group_remove', channel, user)
def parse_guild_member_add(self, data): def parse_guild_member_add(self, data):
guild = self._get_guild(int(data['guild_id'])) guild = self._get_guild(int(data['guild_id']))
if guild is None: if guild is None:
@ -871,7 +845,7 @@ class ConnectionState:
# check if it requires chunking # check if it requires chunking
if self._guild_needs_chunking(guild): if self._guild_needs_chunking(guild):
asyncio.ensure_future(self._chunk_and_dispatch(guild, unavailable), loop=self.loop) asyncio.create_task(self._chunk_and_dispatch(guild, unavailable))
return return
# Dispatch available if newly available # Dispatch available if newly available
@ -880,10 +854,6 @@ class ConnectionState:
else: else:
self.dispatch('guild_join', guild) self.dispatch('guild_join', guild)
def parse_guild_sync(self, data):
guild = self._get_guild(int(data['id']))
guild._sync(data)
def parse_guild_update(self, data): def parse_guild_update(self, data):
guild = self._get_guild(int(data['id'])) guild = self._get_guild(int(data['id']))
if guild is not None: if guild is not None:
@ -1015,7 +985,7 @@ class ConnectionState:
voice = self._get_voice_client(guild.id) voice = self._get_voice_client(guild.id)
if voice is not None: if voice is not None:
coro = voice.on_voice_state_update(data) coro = voice.on_voice_state_update(data)
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler')) asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
member, before, after = guild._update_voice_state(data, channel_id) member, before, after = guild._update_voice_state(data, channel_id)
if member is not None: if member is not None:
@ -1029,11 +999,6 @@ class ConnectionState:
self.dispatch('voice_state_update', member, before, after) self.dispatch('voice_state_update', member, before, after)
else: else:
log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id']) log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id'])
else:
# in here we're either at private or group calls
call = self._calls.get(channel_id)
if call is not None:
call._update_voice_state(data)
def parse_voice_server_update(self, data): def parse_voice_server_update(self, data):
try: try:
@ -1044,7 +1009,7 @@ class ConnectionState:
vc = self._get_voice_client(key_id) vc = self._get_voice_client(key_id)
if vc is not None: if vc is not None:
coro = vc.on_voice_server_update(data) coro = vc.on_voice_server_update(data)
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice server update handler')) asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
def parse_typing_start(self, data): def parse_typing_start(self, data):
channel, guild = self._get_guild_channel(data) channel, guild = self._get_guild_channel(data)
@ -1065,27 +1030,9 @@ class ConnectionState:
if member is not None: if member is not None:
timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp')) timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp'))
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
self.dispatch('typing', channel, member, timestamp) self.dispatch('typing', channel, member, timestamp)
def parse_relationship_add(self, data):
key = int(data['id'])
old = self.user.get_relationship(key)
new = Relationship(state=self, data=data)
self.user._relationships[key] = new
if old is not None:
self.dispatch('relationship_update', old, new)
else:
self.dispatch('relationship_add', new)
def parse_relationship_remove(self, data):
key = int(data['id'])
try:
old = self.user._relationships.pop(key)
except KeyError:
pass
else:
self.dispatch('relationship_remove', old)
def _get_reaction_user(self, channel, user_id): def _get_reaction_user(self, channel, user_id):
if isinstance(channel, TextChannel): if isinstance(channel, TextChannel):
return channel.guild.get_member(user_id) return channel.guild.get_member(user_id)
@ -1223,16 +1170,20 @@ class AutoShardedConnectionState(ConnectionState):
self.user = user = ClientUser(state=self, data=data['user']) self.user = user = ClientUser(state=self, data=data['user'])
self._users[user.id] = user self._users[user.id] = user
if self.application_id is None:
try:
application = data['application']
except KeyError:
pass
else:
self.application_id = utils._get_as_snowflake(application, 'id')
for guild_data in data['guilds']: for guild_data in data['guilds']:
self._add_guild_from_data(guild_data) self._add_guild_from_data(guild_data)
if self._messages: if self._messages:
self._update_message_references() self._update_message_references()
for pm in data.get('private_channels', []):
factory, _ = _channel_factory(pm['type'])
self._add_private_channel(factory(me=user, data=pm, state=self))
self.dispatch('connect') self.dispatch('connect')
self.dispatch('shard_connect', data['__shard_id__']) self.dispatch('shard_connect', data['__shard_id__'])
@ -1245,7 +1196,7 @@ class AutoShardedConnectionState(ConnectionState):
gc.collect() gc.collect()
if self._ready_task is None: if self._ready_task is None:
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop) self._ready_task = asyncio.create_task(self._delay_ready())
def parse_resumed(self, data): def parse_resumed(self, data):
self.dispatch('resumed') self.dispatch('resumed')

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -29,6 +27,10 @@ from .asset import Asset
from .utils import snowflake_time from .utils import snowflake_time
from .enums import StickerType, try_enum from .enums import StickerType, try_enum
__all__ = (
'Sticker',
)
class Sticker(Hashable): class Sticker(Hashable):
"""Represents a sticker. """Represents a sticker.
@ -93,7 +95,7 @@ class Sticker(Hashable):
@property @property
def created_at(self): def created_at(self):
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC as a naive datetime.""" """:class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -43,10 +41,6 @@ class _PartialTemplateState:
self.__state = state self.__state = state
self.http = _FriendlyHttpAttributeErrorHelper() self.http = _FriendlyHttpAttributeErrorHelper()
@property
def is_bot(self):
return self.__state.is_bot
@property @property
def shard_count(self): def shard_count(self):
return self.__state.shard_count return self.__state.shard_count
@ -76,7 +70,7 @@ class _PartialTemplateState:
return [] return []
def __getattr__(self, attr): def __getattr__(self, attr):
raise AttributeError('PartialTemplateState does not support {0!r}.'.format(attr)) raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
class Template: class Template:
"""Represents a Discord template. """Represents a Discord template.
@ -96,9 +90,10 @@ class Template:
creator: :class:`User` creator: :class:`User`
The creator of the template. The creator of the template.
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
When the template was created. An aware datetime in UTC representing when the template was created.
updated_at: :class:`datetime.datetime` updated_at: :class:`datetime.datetime`
When the template was last updated (referred to as "last synced" in the client). An aware datetime in UTC representing when the template was last updated.
This is referred to as "last synced" in the official Discord client.
source_guild: :class:`Guild` source_guild: :class:`Guild`
The source guild. The source guild.
""" """
@ -127,7 +122,7 @@ class Template:
source_serialised['id'] = id source_serialised['id'] = id
state = _PartialTemplateState(state=self._state) state = _PartialTemplateState(state=self._state)
guild = Guild(data=source_serialised, state=state) guild = Guild(data=source_serialised, state=state)
self.source_guild = guild self.source_guild = guild
def __repr__(self): def __repr__(self):

10
discord/types/__init__.py Normal file
View File

@ -0,0 +1,10 @@
"""
discord.types
~~~~~~~~~~~~~~
Typings for the Discord API
:copyright: (c) 2015-present Rapptz
:license: MIT, see LICENSE for more details.
"""

91
discord/types/channel.py Normal file
View File

@ -0,0 +1,91 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .user import PartialUser
from .snowflake import Snowflake
from typing import List, Literal, Optional, TypedDict
class PermissionOverwrite(TypedDict):
id: Snowflake
type: Literal[0, 1]
allow: str
deny: str
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
class PartialChannel(TypedDict):
id: str
type: ChannelType
name: str
class _TextChannelOptional(PartialChannel, total=False):
topic: str
last_message_id: Optional[Snowflake]
last_pin_timestamp: int
rate_limit_per_user: int
class _VoiceChannelOptional(PartialChannel, total=False):
rtc_region: Optional[str]
bitrate: int
user_limit: int
class _CategoryChannelOptional(PartialChannel, total=False):
...
class _StoreChannelOptional(PartialChannel, total=False):
...
class _StageChannelOptional(PartialChannel, total=False):
rtc_region: Optional[str]
bitrate: int
user_limit: int
topic: str
class GuildChannel(
_TextChannelOptional, _VoiceChannelOptional, _CategoryChannelOptional, _StoreChannelOptional, _StageChannelOptional
):
guild_id: Snowflake
position: int
permission_overwrites: List[PermissionOverwrite]
nsfw: bool
parent_id: Optional[Snowflake]
class DMChannel(PartialChannel):
last_message_id: Optional[Snowflake]
recipients: List[PartialUser]
class GroupDMChannel(DMChannel):
icon: Optional[str]
owner_id: Snowflake

84
discord/types/embed.py Normal file
View File

@ -0,0 +1,84 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import List, Literal, TypedDict
class _EmbedFooterOptional(TypedDict, total=False):
icon_url: str
proxy_icon_url: str
class EmbedFooter(_EmbedFooterOptional):
text: str
class _EmbedFieldOptional(TypedDict, total=False):
inline: bool
class EmbedField(_EmbedFieldOptional):
name: str
value: str
class EmbedThumbnail(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedVideo(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedImage(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedProvider(TypedDict, total=False):
name: str
url: str
class EmbedAuthor(TypedDict, total=False):
name: str
url: str
icon_url: str
proxy_icon_url: str
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
class Embed(TypedDict, total=False):
title: str
type: EmbedType
description: str
url: str
timestamp: str
color: int
footer: EmbedFooter
image: EmbedImage
thumbnail: EmbedThumbnail
video: EmbedVideo
provider: EmbedProvider
author: EmbedAuthor
fields: List[EmbedField]

View File

@ -0,0 +1,28 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import List
Snowflake = str
SnowflakeList = List[Snowflake]

33
discord/types/user.py Normal file
View File

@ -0,0 +1,33 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .snowflake import Snowflake
from typing import Optional, TypedDict
class PartialUser(TypedDict):
id: Snowflake
username: str
discriminator: str
avatar: Optional[str]

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -24,62 +22,17 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from collections import namedtuple
import discord.abc import discord.abc
from .flags import PublicUserFlags from .flags import PublicUserFlags
from .utils import snowflake_time, _bytes_to_base64_data, parse_time from .utils import snowflake_time, _bytes_to_base64_data
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse, PremiumType, try_enum from .enums import DefaultAvatar, try_enum
from .errors import ClientException
from .colour import Colour from .colour import Colour
from .asset import Asset from .asset import Asset
from .utils import deprecated
class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')): __all__ = (
__slots__ = () 'User',
'ClientUser',
@property )
def nitro(self):
return self.premium_since is not None
premium = nitro
def _has_flag(self, o):
v = o.value
return (self.flags & v) == v
@property
def staff(self):
return self._has_flag(UserFlags.staff)
@property
def partner(self):
return self._has_flag(UserFlags.partner)
@property
def bug_hunter(self):
return self._has_flag(UserFlags.bug_hunter)
@property
def early_supporter(self):
return self._has_flag(UserFlags.early_supporter)
@property
def hypesquad(self):
return self._has_flag(UserFlags.hypesquad)
@property
def hypesquad_houses(self):
flags = (UserFlags.hypesquad_bravery, UserFlags.hypesquad_brilliance, UserFlags.hypesquad_balance)
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
@property
def team_user(self):
return self._has_flag(UserFlags.team_user)
@property
def system(self):
return self._has_flag(UserFlags.system)
_BaseUser = discord.abc.User _BaseUser = discord.abc.User
@ -199,7 +152,7 @@ class BaseUser(_BaseUser):
@property @property
def default_avatar_url(self): def default_avatar_url(self):
""":class:`Asset`: Returns a URL for a user's default avatar.""" """:class:`Asset`: Returns a URL for a user's default avatar."""
return Asset(self._state, '/embed/avatars/{}.png'.format(self.default_avatar.value)) return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
@property @property
def colour(self): def colour(self):
@ -222,7 +175,7 @@ class BaseUser(_BaseUser):
@property @property
def mention(self): def mention(self):
""":class:`str`: Returns a string that allows you to mention the given user.""" """:class:`str`: Returns a string that allows you to mention the given user."""
return '<@{0.id}>'.format(self) return f'<@{self.id}>'
def permissions_in(self, channel): def permissions_in(self, channel):
"""An alias for :meth:`abc.GuildChannel.permissions_for`. """An alias for :meth:`abc.GuildChannel.permissions_for`.
@ -244,7 +197,8 @@ class BaseUser(_BaseUser):
def created_at(self): def created_at(self):
""":class:`datetime.datetime`: Returns the user's creation time in UTC. """:class:`datetime.datetime`: Returns the user's creation time in UTC.
This is when the user's Discord account was created.""" This is when the user's Discord account was created.
"""
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property
@ -315,33 +269,26 @@ class ClientUser(BaseUser):
.. versionadded:: 1.3 .. versionadded:: 1.3
verified: :class:`bool` verified: :class:`bool`
<<<<<<< HEAD
Specifies if the user's email is verified. Specifies if the user's email is verified.
email: Optional[:class:`str`] email: Optional[:class:`str`]
The email the user used when registering. The email the user used when registering.
.. deprecated:: 1.7 .. deprecated:: 1.7
=======
Specifies if the user is a verified account.
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
locale: Optional[:class:`str`] locale: Optional[:class:`str`]
The IETF language tag used to identify the language the user is using. The IETF language tag used to identify the language the user is using.
mfa_enabled: :class:`bool` mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working. 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__ + \ __slots__ = BaseUser.__slots__ + \
('email', 'locale', '_flags', 'verified', 'mfa_enabled', ('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
'premium', 'premium_type', '_relationships', '__weakref__')
def __init__(self, *, state, data): def __init__(self, *, state, data):
super().__init__(state=state, data=data) super().__init__(state=state, data=data)
self._relationships = {}
def __repr__(self): def __repr__(self):
return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \ return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
@ -351,83 +298,16 @@ class ClientUser(BaseUser):
super()._update(data) super()._update(data)
# There's actually an Optional[str] phone field as well but I won't use it # There's actually an Optional[str] phone field as well but I won't use it
self.verified = data.get('verified', False) self.verified = data.get('verified', False)
self.email = data.get('email')
self.locale = data.get('locale') self.locale = data.get('locale')
self._flags = data.get('flags', 0) self._flags = data.get('flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False) self.mfa_enabled = data.get('mfa_enabled', False)
self.premium = data.get('premium', False)
self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
@deprecated()
def get_relationship(self, user_id):
"""Retrieves the :class:`Relationship` if applicable.
.. deprecated:: 1.7 async def edit(self, *, username=None, avatar=None):
.. note::
This can only be used by non-bot accounts.
Parameters
-----------
user_id: :class:`int`
The user ID to check if we have a relationship with them.
Returns
--------
Optional[:class:`Relationship`]
The relationship if available or ``None``.
"""
return self._relationships.get(user_id)
@property
def relationships(self):
"""List[:class:`User`]: Returns all the relationships that the user has.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
"""
return list(self._relationships.values())
@property
def friends(self):
r"""List[:class:`User`]: Returns all the users that the user is friends with.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
"""
return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend]
@property
def blocked(self):
r"""List[:class:`User`]: Returns all the users that the user has blocked.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
"""
return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked]
async def edit(self, **fields):
"""|coro| """|coro|
Edits the current profile of the client. Edits the current profile of the client.
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:: .. note::
To upload an avatar, a :term:`py:bytes-like object` must be passed in that To upload an avatar, a :term:`py:bytes-like object` must be passed in that
@ -439,19 +319,6 @@ class ClientUser(BaseUser):
Parameters Parameters
----------- -----------
password: :class:`str`
The current password for the client's account.
Only applicable to user accounts.
new_password: :class:`str`
The new password you wish to change to.
Only applicable to user accounts.
email: :class:`str`
The new email you wish to change to.
Only applicable to user accounts.
house: Optional[:class:`HypeSquadHouse`]
The hypesquad house you wish to change to.
Could be ``None`` to leave the current house.
Only applicable to user accounts.
username: :class:`str` username: :class:`str`
The new username you wish to change to. The new username you wish to change to.
avatar: :class:`bytes` avatar: :class:`bytes`
@ -464,218 +331,14 @@ class ClientUser(BaseUser):
Editing your profile failed. Editing your profile failed.
InvalidArgument InvalidArgument
Wrong image format passed for ``avatar``. Wrong image format passed for ``avatar``.
ClientException
Password is required for non-bot accounts.
House field was not a HypeSquadHouse.
""" """
try: if avatar is not None:
avatar_bytes = fields['avatar'] avatar = _bytes_to_base64_data(avatar)
except KeyError:
avatar = self.avatar
else:
if avatar_bytes is not None:
avatar = _bytes_to_base64_data(avatar_bytes)
else:
avatar = None
not_bot_account = not self.bot
password = fields.get('password')
if not_bot_account and password is None:
raise ClientException('Password is required for non-bot accounts.')
args = {
'password': password,
'username': fields.get('username', self.name),
'avatar': avatar
}
if not_bot_account:
args['email'] = fields.get('email', self.email)
if 'new_password' in fields:
args['new_password'] = fields['new_password']
http = self._state.http
if 'house' in fields:
house = fields['house']
if house is None:
await http.leave_hypesquad_house()
elif not isinstance(house, HypeSquadHouse):
raise ClientException('`house` parameter was not a HypeSquadHouse')
else:
value = house.value
await http.change_hypesquad_house(value)
data = await http.edit_profile(**args)
if not_bot_account:
self.email = data['email']
try:
http._token(data['token'], bot=False)
except KeyError:
pass
data = await self._state.http.edit_profile(username=username, avatar=avatar)
self._update(data) self._update(data)
@deprecated()
async def create_group(self, *recipients):
r"""|coro|
Creates a group direct message with the recipients
provided. These recipients must be have a relationship
of type :attr:`RelationshipType.friend`.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
Parameters
-----------
\*recipients: :class:`User`
An argument :class:`list` of :class:`User` to have in
your group.
Raises
-------
HTTPException
Failed to create the group direct message.
ClientException
Attempted to create a group with only one recipient.
This does not include yourself.
Returns
-------
:class:`GroupChannel`
The new group channel.
"""
from .channel import GroupChannel
if len(recipients) < 2:
raise ClientException('You must have two or more recipients to create a group.')
users = [str(u.id) for u in recipients]
data = await self._state.http.start_group(self.id, users)
return GroupChannel(me=self, data=data, state=self._state)
@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.
Parameters
-------
afk_timeout: :class:`int`
How long (in seconds) the user needs to be AFK until Discord
sends push notifications to your mobile device.
animate_emojis: :class:`bool`
Whether or not to animate emojis in the chat.
convert_emoticons: :class:`bool`
Whether or not to automatically convert emoticons into emojis.
e.g. :-) -> 😃
default_guilds_restricted: :class:`bool`
Whether or not to automatically disable DMs between you and
members of new guilds you join.
detect_platform_accounts: :class:`bool`
Whether or not to automatically detect accounts from services
like Steam and Blizzard when you open the Discord client.
developer_mode: :class:`bool`
Whether or not to enable developer mode.
disable_games_tab: :class:`bool`
Whether or not to disable the showing of the Games tab.
enable_tts_command: :class:`bool`
Whether or not to allow tts messages to be played/sent.
explicit_content_filter: :class:`UserContentFilter`
The filter for explicit content in all messages.
friend_source_flags: :class:`FriendFlags`
Who can add you as a friend.
gif_auto_play: :class:`bool`
Whether or not to automatically play gifs that are in the chat.
guild_positions: List[:class:`abc.Snowflake`]
A list of guilds in order of the guild/guild icons that are on
the left hand side of the UI.
inline_attachment_media: :class:`bool`
Whether or not to display attachments when they are uploaded in chat.
inline_embed_media: :class:`bool`
Whether or not to display videos and images from links posted in chat.
locale: :class:`str`
The :rfc:`3066` language identifier of the locale to use for the language
of the Discord client.
message_display_compact: :class:`bool`
Whether or not to use the compact Discord display mode.
render_embeds: :class:`bool`
Whether or not to render embeds that are sent in the chat.
render_reactions: :class:`bool`
Whether or not to render reactions that are added to messages.
restricted_guilds: List[:class:`abc.Snowflake`]
A list of guilds that you will not receive DMs from.
show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing.
status: :class:`Status`
The clients status that is shown to others.
theme: :class:`Theme`
The theme of the Discord UI.
timezone_offset: :class:`int`
The timezone offset to use.
Raises
-------
HTTPException
Editing the settings failed.
Forbidden
The client is a bot user and not a user account.
Returns
-------
:class:`dict`
The client user's updated settings.
"""
payload = {}
content_filter = kwargs.pop('explicit_content_filter', None)
if content_filter:
payload.update({'explicit_content_filter': content_filter.value})
friend_flags = kwargs.pop('friend_source_flags', None)
if friend_flags:
dicts = [{}, {'mutual_guilds': True}, {'mutual_friends': True},
{'mutual_guilds': True, 'mutual_friends': True}, {'all': True}]
payload.update({'friend_source_flags': dicts[friend_flags.value]})
guild_positions = kwargs.pop('guild_positions', None)
if guild_positions:
guild_positions = [str(x.id) for x in guild_positions]
payload.update({'guild_positions': guild_positions})
restricted_guilds = kwargs.pop('restricted_guilds', None)
if restricted_guilds:
restricted_guilds = [str(x.id) for x in restricted_guilds]
payload.update({'restricted_guilds': restricted_guilds})
status = kwargs.pop('status', None)
if status:
payload.update({'status': status.value})
theme = kwargs.pop('theme', None)
if theme:
payload.update({'theme': theme.value})
payload.update(kwargs)
data = await self._state.http.edit_settings(**payload)
return data
class User(BaseUser, discord.abc.Messageable): class User(BaseUser, discord.abc.Messageable):
"""Represents a Discord user. """Represents a Discord user.
@ -763,197 +426,3 @@ class User(BaseUser, discord.abc.Messageable):
state = self._state state = self._state
data = await state.http.start_private_message(self.id) data = await state.http.start_private_message(self.id)
return state.add_dm_channel(data) return state.add_dm_channel(data)
@property
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.
Raises
-------
Forbidden
Not allowed to get mutual friends of this user.
HTTPException
Getting mutual friends failed.
Returns
-------
List[:class:`User`]
The users that are mutual friends.
"""
state = self._state
mutuals = await state.http.get_mutual_friends(self.id)
return [User(state=state, data=friend) for friend in mutuals]
@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.
"""
r = self.relationship
if r is None:
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.
"""
r = self.relationship
if r is None:
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.
Raises
-------
Forbidden
Not allowed to block this user.
HTTPException
Blocking the user failed.
"""
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
@deprecated()
async def unblock(self):
"""|coro|
Unblocks the user.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
Raises
-------
Forbidden
Not allowed to unblock this user.
HTTPException
Unblocking the user failed.
"""
await self._state.http.remove_relationship(self.id)
@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.
Raises
-------
Forbidden
Not allowed to remove this user as a friend.
HTTPException
Removing the user as a friend failed.
"""
await self._state.http.remove_relationship(self.id)
@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.
Raises
-------
Forbidden
Not allowed to send a friend request to the user.
HTTPException
Sending the friend request failed.
"""
await self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator)
@deprecated()
async def profile(self):
"""|coro|
Gets the user's profile.
.. deprecated:: 1.7
.. note::
This can only be used by non-bot accounts.
Raises
-------
Forbidden
Not allowed to fetch profiles.
HTTPException
Fetching the profile failed.
Returns
--------
:class:`Profile`
The profile of the user.
"""
state = self._state
data = await state.http.get_user_profile(self.id)
def transform(d):
return state._get_guild(int(d['id']))
since = data.get('premium_since')
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
return Profile(flags=data['user'].get('flags', 0),
premium_since=parse_time(since),
mutual_guilds=mutual_guilds,
user=self,
connected_accounts=data['connected_accounts'])

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -27,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
import array import array
import asyncio import asyncio
import collections.abc import collections.abc
from typing import Optional, overload
import unicodedata import unicodedata
from base64 import b64encode from base64 import b64encode
from bisect import bisect_left from bisect import bisect_left
@ -40,8 +39,19 @@ import warnings
from .errors import InvalidArgument from .errors import InvalidArgument
__all__ = (
'oauth_uri',
'snowflake_time',
'time_snowflake',
'find',
'get',
'sleep_until',
'utcnow',
'remove_markdown',
'escape_markdown',
'escape_mentions',
)
DISCORD_EPOCH = 1420070400000 DISCORD_EPOCH = 1420070400000
MAX_ASYNCIO_SECONDS = 3456000
class cached_property: class cached_property:
def __init__(self, function): def __init__(self, function):
@ -105,9 +115,17 @@ class SequenceProxy(collections.abc.Sequence):
def count(self, value): def count(self, value):
return self.__proxied.count(value) return self.__proxied.count(value)
def parse_time(timestamp): @overload
def parse_time(timestamp: None) -> None:
...
@overload
def parse_time(timestamp: str) -> datetime.datetime:
...
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
if timestamp: if timestamp:
return datetime.datetime(*map(int, re.split(r'[^\d]', timestamp.replace('+00:00', '')))) return datetime.datetime.fromisoformat(timestamp)
return None return None
def copy_doc(original): def copy_doc(original):
@ -158,7 +176,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
:class:`str` :class:`str`
The OAuth2 URL for inviting the bot into guilds. The OAuth2 URL for inviting the bot into guilds.
""" """
url = 'https://discord.com/oauth2/authorize?client_id={}'.format(client_id) url = f'https://discord.com/oauth2/authorize?client_id={client_id}'
url = url + '&scope=' + '+'.join(scopes or ('bot',)) url = url + '&scope=' + '+'.join(scopes or ('bot',))
if permissions is not None: if permissions is not None:
url = url + '&permissions=' + str(permissions.value) url = url + '&permissions=' + str(permissions.value)
@ -170,7 +188,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
return url return url
def snowflake_time(id): def snowflake_time(id: int) -> datetime.datetime:
""" """
Parameters Parameters
----------- -----------
@ -180,25 +198,34 @@ def snowflake_time(id):
Returns Returns
-------- --------
:class:`datetime.datetime` :class:`datetime.datetime`
The creation date in UTC of a Discord snowflake ID.""" An aware datetime in UTC representing the creation time of the snowflake.
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000) """
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
def time_snowflake(datetime_obj, high=False): def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
"""Returns a numeric snowflake pretending to be created at the given date. """Returns a numeric snowflake pretending to be created at the given date.
When using as the lower end of a range, use ``time_snowflake(high=False) - 1`` to be inclusive, ``high=True`` to be exclusive When using as the lower end of a range, use ``time_snowflake(high=False) - 1``
When using as the higher end of a range, use ``time_snowflake(high=True)`` + 1 to be inclusive, ``high=False`` to be exclusive to be inclusive, ``high=True`` to be exclusive.
When using as the higher end of a range, use ``time_snowflake(high=True) + 1``
to be inclusive, ``high=False`` to be exclusive
Parameters Parameters
----------- -----------
datetime_obj: :class:`datetime.datetime` dt: :class:`datetime.datetime`
A timezone-naive datetime object representing UTC time. A datetime object to convert to a snowflake.
If naive, the timezone is assumed to be local time.
high: :class:`bool` high: :class:`bool`
Whether or not to set the lower 22 bit to high or low. Whether or not to set the lower 22 bit to high or low.
"""
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
Returns
--------
:class:`int`
The snowflake representing the time given.
"""
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
return (discord_millis << 22) + (2**22-1 if high else 0) return (discord_millis << 22) + (2**22-1 if high else 0)
def find(predicate, seq): def find(predicate, seq):
@ -376,19 +403,31 @@ async def sleep_until(when, result=None):
----------- -----------
when: :class:`datetime.datetime` when: :class:`datetime.datetime`
The timestamp in which to sleep until. If the datetime is naive then The timestamp in which to sleep until. If the datetime is naive then
it is assumed to be in UTC. it is assumed to be local time.
result: Any result: Any
If provided is returned to the caller when the coroutine completes. If provided is returned to the caller when the coroutine completes.
""" """
if when.tzinfo is None: if when.tzinfo is None:
when = when.replace(tzinfo=datetime.timezone.utc) when = when.astimezone()
now = datetime.datetime.now(datetime.timezone.utc) now = datetime.datetime.now(datetime.timezone.utc)
delta = (when - now).total_seconds() delta = (when - now).total_seconds()
while delta > MAX_ASYNCIO_SECONDS:
await asyncio.sleep(MAX_ASYNCIO_SECONDS)
delta -= MAX_ASYNCIO_SECONDS
return await asyncio.sleep(max(delta, 0), result) return await asyncio.sleep(max(delta, 0), result)
def utcnow() -> datetime.datetime:
"""A helper function to return an aware UTC datetime representing the current time.
This should be preferred to :func:`datetime.datetime.utcnow` since it is an aware
datetime, compared to the naive datetime in the standard library.
.. versionadded:: 2.0
Returns
--------
:class:`datetime.datetime`
The current aware datetime in UTC.
"""
return datetime.datetime.now(datetime.timezone.utc)
def valid_icon_size(size): def valid_icon_size(size):
"""Icons must be power of 2 within [16, 4096].""" """Icons must be power of 2 within [16, 4096]."""
return not size & (size - 1) and size in range(16, 4097) return not size & (size - 1) and size in range(16, 4097)
@ -489,21 +528,21 @@ _MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)' _MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
_MARKDOWN_ESCAPE_REGEX = re.compile(r'(?P<markdown>%s|%s)' % (_MARKDOWN_ESCAPE_SUBREGEX, _MARKDOWN_ESCAPE_COMMON), re.MULTILINE) _MARKDOWN_ESCAPE_REGEX = re.compile(fr'(?P<markdown>{_MARKDOWN_ESCAPE_SUBREGEX}|{_MARKDOWN_ESCAPE_COMMON})', re.MULTILINE)
_URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])' _URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
_MARKDOWN_STOCK_REGEX = r'(?P<markdown>[_\\~|\*`]|%s)' % _MARKDOWN_ESCAPE_COMMON _MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
def remove_markdown(text, *, ignore_links=True): def remove_markdown(text, *, ignore_links=True):
"""A helper function that removes markdown characters. """A helper function that removes markdown characters.
.. versionadded:: 1.7 .. versionadded:: 1.7
.. note:: .. note::
This function is not markdown aware and may remove meaning from the original text. For example, 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``. if the input contains ``10 * 5`` then it will be converted into ``10 5``.
Parameters Parameters
----------- -----------
text: :class:`str` text: :class:`str`
@ -525,7 +564,7 @@ def remove_markdown(text, *, ignore_links=True):
regex = _MARKDOWN_STOCK_REGEX regex = _MARKDOWN_STOCK_REGEX
if ignore_links: if ignore_links:
regex = '(?:%s|%s)' % (_URL_REGEX, regex) regex = f'(?:{_URL_REGEX}|{regex})'
return re.sub(regex, replacement, text, 0, re.MULTILINE) return re.sub(regex, replacement, text, 0, re.MULTILINE)
def escape_markdown(text, *, as_needed=False, ignore_links=True): def escape_markdown(text, *, as_needed=False, ignore_links=True):
@ -563,7 +602,7 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
regex = _MARKDOWN_STOCK_REGEX regex = _MARKDOWN_STOCK_REGEX
if ignore_links: if ignore_links:
regex = '(?:%s|%s)' % (_URL_REGEX, regex) regex = f'(?:{_URL_REGEX}|{regex})'
return re.sub(regex, replacement, text, 0, re.MULTILINE) return re.sub(regex, replacement, text, 0, re.MULTILINE)
else: else:
text = re.sub(r'\\', r'\\\\', text) text = re.sub(r'\\', r'\\\\', text)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -57,6 +55,11 @@ try:
except ImportError: except ImportError:
has_nacl = False has_nacl = False
__all__ = (
'VoiceProtocol',
'VoiceClient',
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VoiceProtocol: class VoiceProtocol:
@ -558,7 +561,7 @@ class VoiceClient(VoiceProtocol):
raise ClientException('Already playing audio.') raise ClientException('Already playing audio.')
if not isinstance(source, AudioSource): if not isinstance(source, AudioSource):
raise TypeError('source must an AudioSource not {0.__class__.__name__}'.format(source)) raise TypeError(f'source must an AudioSource not {source.__class__.__name__}')
if not self.encoder and not source.is_opus(): if not self.encoder and not source.is_opus():
self.encoder = opus.Encoder() self.encoder = opus.Encoder()
@ -601,7 +604,7 @@ class VoiceClient(VoiceProtocol):
@source.setter @source.setter
def source(self, value): def source(self, value):
if not isinstance(value, AudioSource): if not isinstance(value, AudioSource):
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(value)) raise TypeError(f'expected AudioSource not {value.__class__.__name__}.')
if self._player is None: if self._player is None:
raise ValueError('Not playing anything.') raise ValueError('Not playing anything.')

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -47,10 +45,104 @@ __all__ = (
'RequestsWebhookAdapter', 'RequestsWebhookAdapter',
'Webhook', 'Webhook',
'WebhookMessage', 'WebhookMessage',
'PartialWebhookChannel',
'PartialWebhookGuild'
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class PartialWebhookChannel(Hashable):
"""Represents a partial channel for webhooks.
These are typically given for channel follower webhooks.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The partial channel's ID.
name: :class:`str`
The partial channel's name.
"""
__slots__ = ('id', 'name')
def __init__(self, *, data):
self.id = int(data['id'])
self.name = data['name']
def __repr__(self):
return f'<PartialWebhookChannel name={self.name!r} id={self.id}>'
class PartialWebhookGuild(Hashable):
"""Represents a partial guild for webhooks.
These are typically given for channel follower webhooks.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The partial guild's ID.
name: :class:`str`
The partial guild's name.
icon: :class:`str`
The partial guild's icon
"""
__slots__ = ('id', 'name', 'icon', '_state')
def __init__(self, *, data, state):
self._state = state
self.id = int(data['id'])
self.name = data['name']
self.icon = data['icon']
def __repr__(self):
return f'<PartialWebhookGuild name={self.name!r} id={self.id}>'
@property
def icon_url(self):
""":class:`Asset`: Returns the guild's icon asset."""
return self.icon_url_as()
def is_icon_animated(self):
""":class:`bool`: Returns True if the guild has an animated icon."""
return bool(self.icon and self.icon.startswith('a_'))
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
"""Returns an :class:`Asset` for the guild's icon.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 4096.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the icon to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
icon being animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated icons to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
class WebhookAdapter: class WebhookAdapter:
"""Base class for all webhook adapters. """Base class for all webhook adapters.
@ -65,7 +157,7 @@ class WebhookAdapter:
def _prepare(self, webhook): def _prepare(self, webhook):
self._webhook_id = webhook.id self._webhook_id = webhook.id
self._webhook_token = webhook.token self._webhook_token = webhook.token
self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token) self._request_url = f'{self.BASE}/webhooks/{webhook.id}/{webhook.token}'
self.webhook = webhook self.webhook = webhook
def is_async(self): def is_async(self):
@ -100,10 +192,10 @@ class WebhookAdapter:
return self.request('PATCH', self._request_url, payload=payload, reason=reason) return self.request('PATCH', self._request_url, payload=payload, reason=reason)
def edit_webhook_message(self, message_id, payload): def edit_webhook_message(self, message_id, payload):
return self.request('PATCH', '{}/messages/{}'.format(self._request_url, message_id), payload=payload) return self.request('PATCH', f'{self._request_url}/messages/{message_id}', payload=payload)
def delete_webhook_message(self, message_id): def delete_webhook_message(self, message_id):
return self.request('DELETE', '{}/messages/{}'.format(self._request_url, message_id)) return self.request('DELETE', f'{self._request_url}/messages/{message_id}')
def handle_execution_response(self, data, *, wait): def handle_execution_response(self, data, *, wait):
"""Transforms the webhook execution response into something """Transforms the webhook execution response into something
@ -158,7 +250,7 @@ class WebhookAdapter:
multipart = None multipart = None
files_to_pass = None files_to_pass = None
url = '%s?wait=%d' % (self._request_url, wait) url = f'{self._request_url}?wait={int(wait)}'
maybe_coro = None maybe_coro = None
try: try:
maybe_coro = self.request('POST', url, multipart=multipart, payload=data, files=files_to_pass) maybe_coro = self.request('POST', url, multipart=multipart, payload=data, files=files_to_pass)
@ -405,10 +497,6 @@ class _PartialWebhookState:
def store_user(self, data): def store_user(self, data):
return BaseUser(state=self, data=data) return BaseUser(state=self, data=data)
@property
def is_bot(self):
return True
@property @property
def http(self): def http(self):
if self.parent is not None: if self.parent is not None:
@ -422,7 +510,7 @@ class _PartialWebhookState:
if self.parent is not None: if self.parent is not None:
return getattr(self.parent, attr) return getattr(self.parent, attr)
raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr)) raise AttributeError(f'PartialWebhookState does not support {attr!r}.')
class WebhookMessage(Message): class WebhookMessage(Message):
"""Represents a message sent from your webhook. """Represents a message sent from your webhook.
@ -483,7 +571,7 @@ class WebhookMessage(Message):
except HTTPException: except HTTPException:
pass pass
asyncio.ensure_future(inner_call(), loop=self._state.loop) asyncio.create_task(inner_call())
return await asyncio.sleep(0) return await asyncio.sleep(0)
def delete(self, *, delay=None): def delete(self, *, delay=None):
@ -597,10 +685,21 @@ class Webhook(Hashable):
The default name of the webhook. The default name of the webhook.
avatar: Optional[:class:`str`] avatar: Optional[:class:`str`]
The default avatar of the webhook. The default avatar of the webhook.
source_guild: Optional[:class:`PartialWebhookGuild`]
The guild of the channel that this webhook is following.
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
.. versionadded:: 2.0
source_channel: Optional[:class:`PartialWebhookChannel`]
The channel that this webhook is following.
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
.. versionadded:: 2.0
""" """
__slots__ = ('id', 'type', 'guild_id', 'channel_id', 'user', 'name', __slots__ = ('id', 'type', 'guild_id', 'channel_id', 'user', 'name',
'avatar', 'token', '_state', '_adapter') 'avatar', 'token', '_state', '_adapter', 'source_channel', 'source_guild')
def __init__(self, data, *, adapter, state=None): def __init__(self, data, *, adapter, state=None):
self.id = int(data['id']) self.id = int(data['id'])
@ -622,13 +721,25 @@ class Webhook(Hashable):
else: else:
self.user = User(state=state, data=user) self.user = User(state=state, data=user)
source_channel = data.get('source_channel')
if source_channel:
source_channel = PartialWebhookChannel(data=source_channel)
self.source_channel = source_channel
source_guild = data.get('source_guild')
if source_guild:
source_guild = PartialWebhookGuild(data=source_guild, state=state)
self.source_guild = source_guild
def __repr__(self): def __repr__(self):
return '<Webhook id=%r>' % self.id return f'<Webhook id={self.id!r}>'
@property @property
def url(self): def url(self):
""":class:`str` : Returns the webhook's url.""" """:class:`str` : Returns the webhook's url."""
return 'https://discord.com/api/webhooks/{}/{}'.format(self.id, self.token) return f'https://discord.com/api/webhooks/{self.id}/{self.token}'
@classmethod @classmethod
def partial(cls, id, token, *, adapter): def partial(cls, id, token, *, adapter):
@ -697,7 +808,7 @@ class Webhook(Hashable):
@classmethod @classmethod
def _as_follower(cls, data, *, channel, user): def _as_follower(cls, data, *, channel, user):
name = "{} #{}".format(channel.guild, channel) name = f"{channel.guild} #{channel}"
feed = { feed = {
'id': data['webhook_id'], 'id': data['webhook_id'],
'type': 2, 'type': 2,

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
The MIT License (MIT) The MIT License (MIT)
@ -30,6 +28,12 @@ from .activity import create_activity
from .invite import Invite from .invite import Invite
from .enums import Status, try_enum from .enums import Status, try_enum
__all__ = (
'WidgetChannel',
'WidgetMember',
'Widget',
)
class WidgetChannel: class WidgetChannel:
"""Represents a "partial" widget channel. """Represents a "partial" widget channel.
@ -77,7 +81,7 @@ class WidgetChannel:
@property @property
def mention(self): def mention(self):
""":class:`str`: The string that allows you to mention the channel.""" """:class:`str`: The string that allows you to mention the channel."""
return '<#%s>' % self.id return f'<#{self.id}>'
@property @property
def created_at(self): def created_at(self):
@ -236,7 +240,7 @@ class Widget:
@property @property
def json_url(self): def json_url(self):
""":class:`str`: The JSON URL of the widget.""" """:class:`str`: The JSON URL of the widget."""
return "https://discord.com/api/guilds/{0.id}/widget.json".format(self) return f"https://discord.com/api/guilds/{self.id}/widget.json"
@property @property
def invite_url(self): def invite_url(self):

View File

@ -340,7 +340,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:type channel: :class:`abc.Messageable` :type channel: :class:`abc.Messageable`
:param user: The user that started typing. :param user: The user that started typing.
:type user: Union[:class:`User`, :class:`Member`] :type user: Union[:class:`User`, :class:`Member`]
:param when: When the typing started as a naive datetime in UTC. :param when: When the typing started as an aware datetime in UTC.
:type when: :class:`datetime.datetime` :type when: :class:`datetime.datetime`
.. function:: on_message(message) .. function:: on_message(message)
@ -585,6 +585,17 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param payload: The raw event payload data. :param payload: The raw event payload data.
:type payload: :class:`RawReactionClearEmojiEvent` :type payload: :class:`RawReactionClearEmojiEvent`
.. function:: on_interaction(interaction)
Called when an interaction happened.
This currently happens due to slash command invocations.
.. versionadded:: 2.0
:param interaction: The interaction data.
:type interaction: :class:`Interaction`
.. function:: on_private_channel_delete(channel) .. function:: on_private_channel_delete(channel)
on_private_channel_create(channel) on_private_channel_create(channel)
@ -612,7 +623,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param channel: The private channel that had its pins updated. :param channel: The private channel that had its pins updated.
:type channel: :class:`abc.PrivateChannel` :type channel: :class:`abc.PrivateChannel`
:param last_pin: The latest message that was pinned as a naive datetime in UTC. Could be ``None``. :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``.
:type last_pin: Optional[:class:`datetime.datetime`] :type last_pin: Optional[:class:`datetime.datetime`]
.. function:: on_guild_channel_delete(channel) .. function:: on_guild_channel_delete(channel)
@ -646,7 +657,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param channel: The guild channel that had its pins updated. :param channel: The guild channel that had its pins updated.
:type channel: :class:`abc.GuildChannel` :type channel: :class:`abc.GuildChannel`
:param last_pin: The latest message that was pinned as a naive datetime in UTC. Could be ``None``. :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``.
:type last_pin: Optional[:class:`datetime.datetime`] :type last_pin: Optional[:class:`datetime.datetime`]
.. function:: on_guild_integrations_update(guild) .. function:: on_guild_integrations_update(guild)
@ -897,29 +908,6 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param user: The user that joined or left. :param user: The user that joined or left.
:type user: :class:`User` :type user: :class:`User`
.. function:: on_relationship_add(relationship)
on_relationship_remove(relationship)
Called when a :class:`Relationship` is added or removed from the
:class:`ClientUser`.
.. deprecated:: 1.7
:param relationship: The relationship that was added or removed.
:type relationship: :class:`Relationship`
.. function:: on_relationship_update(before, after)
Called when a :class:`Relationship` is updated, e.g. when you
block a friend or a friendship is accepted.
.. deprecated:: 1.7
:param before: The previous relationship status.
:type before: :class:`Relationship`
:param after: The updated relationship status.
:type after: :class:`Relationship`
.. _discord-api-utils: .. _discord-api-utils:
Utility Functions Utility Functions
@ -945,96 +933,7 @@ Utility Functions
.. autofunction:: discord.utils.sleep_until .. autofunction:: discord.utils.sleep_until
Profile .. autofunction:: discord.utils.utcnow
---------
.. class:: Profile
A namedtuple representing a user's Discord public profile.
.. deprecated:: 1.7
.. attribute:: user
The :class:`User` the profile belongs to.
:type: :class:`User`
.. attribute:: premium
A boolean indicating if the user has premium (i.e. Discord Nitro).
:type: :class:`bool`
.. attribute:: nitro
An alias for :attr:`premium`.
.. attribute:: premium_since
A naive UTC datetime indicating how long the user has been premium since.
This could be ``None`` if not applicable.
:type: :class:`datetime.datetime`
.. attribute:: staff
A boolean indicating if the user is Discord Staff.
:type: :class:`bool`
.. attribute:: partner
A boolean indicating if the user is a Discord Partner.
:type: :class:`bool`
.. attribute:: bug_hunter
A boolean indicating if the user is a Bug Hunter.
:type: :class:`bool`
.. attribute:: early_supporter
A boolean indicating if the user has had premium before 10 October, 2018.
:type: :class:`bool`
.. attribute:: hypesquad
A boolean indicating if the user is in Discord HypeSquad.
:type: :class:`bool`
.. attribute:: hypesquad_houses
A list of :class:`HypeSquadHouse` that the user is in.
:type: List[:class:`HypeSquadHouse`]
.. attribute:: team_user
A boolean indicating if the user is in part of a team.
.. versionadded:: 1.3
:type: :class:`bool`
.. attribute:: system
A boolean indicating if the user is officially part of the Discord urgent message system.
.. versionadded:: 1.3
:type: :class:`bool`
.. attribute:: mutual_guilds
A list of :class:`Guild` that the :class:`ClientUser` shares with this
user.
:type: List[:class:`Guild`]
.. attribute:: connected_accounts
A list of dict objects indicating the accounts the user has connected.
An example entry can be seen below: ::
{"type": "twitch", "id": "92473777", "name": "discordapp"}
:type: List[Dict[:class:`str`, :class:`str`]]
.. _discord-api-enums: .. _discord-api-enums:
@ -1201,6 +1100,20 @@ of :class:`enum.Enum`.
.. versionadded:: 1.5 .. versionadded:: 1.5
.. class:: InteractionType
Specifies the type of :class:`Interaction`.
.. versionadded:: 2.0
.. attribute:: ping
Represents Discord pinging to see if the interaction response server is alive.
.. attribute:: application_command
Represents a slash command interaction.
.. class:: HypeSquadHouse .. class:: HypeSquadHouse
Specifies the HypeSquad house a user belongs to. Specifies the HypeSquad house a user belongs to.
@ -1940,127 +1853,6 @@ of :class:`enum.Enum`.
The action is the update of something. The action is the update of something.
.. class:: RelationshipType
Specifies the type of :class:`Relationship`.
.. deprecated:: 1.7
.. note::
This only applies to users, *not* bots.
.. attribute:: friend
You are friends with this user.
.. attribute:: blocked
You have blocked this user.
.. attribute:: incoming_request
The user has sent you a friend request.
.. attribute:: outgoing_request
You have sent a friend request to this user.
.. class:: UserContentFilter
Represents the options found in ``Settings > Privacy & Safety > Safe Direct Messaging``
in the Discord client.
.. deprecated:: 1.7
.. note::
This only applies to users, *not* bots.
.. attribute:: all_messages
Scan all direct messages from everyone.
.. attribute:: friends
Scan all direct messages that aren't from friends.
.. attribute:: disabled
Don't scan any direct messages.
.. class:: FriendFlags
Represents the options found in ``Settings > Privacy & Safety > Who Can Add You As A Friend``
in the Discord client.
.. deprecated:: 1.7
.. note::
This only applies to users, *not* bots.
.. attribute:: noone
This allows no-one to add you as a friend.
.. attribute:: mutual_guilds
This allows guild members to add you as a friend.
.. attribute:: mutual_friends
This allows friends of friends to add you as a friend.
.. attribute:: guild_and_friends
This is a superset of :attr:`mutual_guilds` and :attr:`mutual_friends`.
.. attribute:: everyone
This allows everyone to add you as a friend.
.. class:: PremiumType
Represents the user's Discord Nitro subscription type.
.. deprecated:: 1.7
.. note::
This only applies to users, *not* bots.
.. attribute:: nitro
Represents the Discord Nitro with Nitro-exclusive games.
.. attribute:: nitro_classic
Represents the Discord Nitro with no Nitro-exclusive games.
.. class:: Theme
Represents the theme synced across all Discord clients.
.. deprecated:: 1.7
.. note::
This only applies to users, *not* bots.
.. attribute:: light
Represents the Light theme on Discord.
.. attribute:: dark
Represents the Dark theme on Discord.
.. class:: TeamMembershipState .. class:: TeamMembershipState
Represents the membership state of a team member retrieved through :func:`Bot.application_info`. Represents the membership state of a team member retrieved through :func:`Bot.application_info`.
@ -2734,20 +2526,19 @@ interface, :meth:`WebhookAdapter.request`.
Abstract Base Classes Abstract Base Classes
----------------------- -----------------------
An :term:`py:abstract base class` (also known as an ``abc``) is a class that models can inherit An :term:`abstract base class` (also known as an ``abc``) is a class that models can inherit
to get their behaviour. The Python implementation of an :doc:`abc <py:library/abc>` is to get their behaviour. **Abstract base classes should not be instantiated**.
slightly different in that you can register them at run-time. **Abstract base classes cannot be instantiated**. They are mainly there for usage with :func:`isinstance` and :func:`issubclass`\.
They are mainly there for usage with :func:`py:isinstance` and :func:`py:issubclass`\.
This library has a module related to abstract base classes, some of which are actually from the :doc:`abc <py:library/abc>` standard This library has a module related to abstract base classes, in which all the ABCs are subclasses of
module, others which are not. :class:`typing.Protocol`.
Snowflake Snowflake
~~~~~~~~~~ ~~~~~~~~~~
.. attributetable:: discord.abc.Snowflake .. attributetable:: discord.abc.Snowflake
.. autoclass:: discord.abc.Snowflake .. autoclass:: discord.abc.Snowflake()
:members: :members:
User User
@ -2755,7 +2546,7 @@ User
.. attributetable:: discord.abc.User .. attributetable:: discord.abc.User
.. autoclass:: discord.abc.User .. autoclass:: discord.abc.User()
:members: :members:
PrivateChannel PrivateChannel
@ -2763,7 +2554,7 @@ PrivateChannel
.. attributetable:: discord.abc.PrivateChannel .. attributetable:: discord.abc.PrivateChannel
.. autoclass:: discord.abc.PrivateChannel .. autoclass:: discord.abc.PrivateChannel()
:members: :members:
GuildChannel GuildChannel
@ -2771,7 +2562,7 @@ GuildChannel
.. attributetable:: discord.abc.GuildChannel .. attributetable:: discord.abc.GuildChannel
.. autoclass:: discord.abc.GuildChannel .. autoclass:: discord.abc.GuildChannel()
:members: :members:
Messageable Messageable
@ -2779,7 +2570,7 @@ Messageable
.. attributetable:: discord.abc.Messageable .. attributetable:: discord.abc.Messageable
.. autoclass:: discord.abc.Messageable .. autoclass:: discord.abc.Messageable()
:members: :members:
:exclude-members: history, typing :exclude-members: history, typing
@ -2794,7 +2585,7 @@ Connectable
.. attributetable:: discord.abc.Connectable .. attributetable:: discord.abc.Connectable
.. autoclass:: discord.abc.Connectable .. autoclass:: discord.abc.Connectable()
.. _discord_api_models: .. _discord_api_models:
@ -2832,14 +2623,6 @@ ClientUser
:members: :members:
:inherited-members: :inherited-members:
Relationship
~~~~~~~~~~~~~~
.. attributetable:: Relationship
.. autoclass:: Relationship()
:members:
User User
~~~~~ ~~~~~
@ -2901,22 +2684,6 @@ Reaction
.. automethod:: users .. automethod:: users
:async-for: :async-for:
CallMessage
~~~~~~~~~~~~
.. attributetable:: CallMessage
.. autoclass:: CallMessage()
:members:
GroupCall
~~~~~~~~~~
.. attributetable:: GroupCall
.. autoclass:: GroupCall()
:members:
Guild Guild
~~~~~~ ~~~~~~
@ -2957,6 +2724,14 @@ Integration
.. autoclass:: IntegrationAccount() .. autoclass:: IntegrationAccount()
:members: :members:
Interaction
~~~~~~~~~~~~
.. attributetable:: Interaction
.. autoclass:: Interaction()
:members:
Member Member
~~~~~~ ~~~~~~
@ -3218,6 +2993,21 @@ RawReactionClearEmojiEvent
.. autoclass:: RawReactionClearEmojiEvent() .. autoclass:: RawReactionClearEmojiEvent()
:members: :members:
PartialWebhookGuild
~~~~~~~~~~~~~~~~~~~~
.. attributetable:: PartialWebhookGuild
.. autoclass:: PartialWebhookGuild()
:members:
PartialWebhookChannel
~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: PartialWebhookChannel
.. autoclass:: PartialWebhookChannel()
:members:
.. _discord_api_data: .. _discord_api_data:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# discord.py documentation build configuration file, created by # discord.py documentation build configuration file, created by
# sphinx-quickstart on Fri Aug 21 05:43:30 2015. # sphinx-quickstart on Fri Aug 21 05:43:30 2015.
@ -44,6 +43,7 @@ extensions = [
] ]
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
autodoc_typehints = 'none'
extlinks = { extlinks = {
'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-'), 'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-'),
@ -76,8 +76,8 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'discord.py' project = 'discord.py'
copyright = u'2015-present, Rapptz' copyright = '2015-present, Rapptz'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -160,7 +160,7 @@ resource_links = {
'discord': 'https://discord.gg/r3sSKJJ', 'discord': 'https://discord.gg/r3sSKJJ',
'issues': 'https://github.com/Rapptz/discord.py/issues', 'issues': 'https://github.com/Rapptz/discord.py/issues',
'discussions': 'https://github.com/Rapptz/discord.py/discussions', 'discussions': 'https://github.com/Rapptz/discord.py/discussions',
'examples': 'https://github.com/Rapptz/discord.py/tree/%s/examples' % branch, 'examples': f'https://github.com/Rapptz/discord.py/tree/{branch}/examples',
} }
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
@ -283,8 +283,8 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
('index', 'discord.py.tex', u'discord.py Documentation', ('index', 'discord.py.tex', 'discord.py Documentation',
u'Rapptz', 'manual'), 'Rapptz', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -313,8 +313,8 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'discord.py', u'discord.py Documentation', ('index', 'discord.py', 'discord.py Documentation',
[u'Rapptz'], 1) ['Rapptz'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@ -327,8 +327,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'discord.py', u'discord.py Documentation', ('index', 'discord.py', 'discord.py Documentation',
u'Rapptz', 'discord.py', 'One line description of project.', 'Rapptz', 'discord.py', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View File

@ -379,7 +379,10 @@ A lot of discord models work out of the gate as a parameter:
- :class:`PartialMessage` (since v1.7) - :class:`PartialMessage` (since v1.7)
- :class:`TextChannel` - :class:`TextChannel`
- :class:`VoiceChannel` - :class:`VoiceChannel`
<<<<<<< HEAD
- :class:`StageChannel` (since v1.7) - :class:`StageChannel` (since v1.7)
=======
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
- :class:`StoreChannel` (since v1.7) - :class:`StoreChannel` (since v1.7)
- :class:`CategoryChannel` - :class:`CategoryChannel`
- :class:`Invite` - :class:`Invite`
@ -411,10 +414,15 @@ converter is given below:
+--------------------------+-------------------------------------------------+ +--------------------------+-------------------------------------------------+
| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` | | :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` |
+--------------------------+-------------------------------------------------+ +--------------------------+-------------------------------------------------+
<<<<<<< HEAD
| :class:`StageChannel` | :class:`~ext.commands.StageChannelConverter` | | :class:`StageChannel` | :class:`~ext.commands.StageChannelConverter` |
+--------------------------+-------------------------------------------------+ +--------------------------+-------------------------------------------------+
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` | | :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
+--------------------------+-------------------------------------------------+ +--------------------------+-------------------------------------------------+
=======
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
+--------------------------+-------------------------------------------------+
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` | | :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
+--------------------------+-------------------------------------------------+ +--------------------------+-------------------------------------------------+
| :class:`Invite` | :class:`~ext.commands.InviteConverter` | | :class:`Invite` | :class:`~ext.commands.InviteConverter` |

View File

@ -32,6 +32,10 @@ Likewise, **this is the last version to support user bots**.
Development of v2.0 will have breaking changes and support for newer API features. Development of v2.0 will have breaking changes and support for newer API features.
<<<<<<< HEAD
=======
>>>>>>> 523e35e4f3c3c49d4e471359f9fb559242bbecc8
- Add support for stage channels via :class:`StageChannel` (:issue:`6602`, :issue:`6608`) - Add support for stage channels via :class:`StageChannel` (:issue:`6602`, :issue:`6608`)
- Add support for :attr:`MessageReference.fail_if_not_exists` (:issue:`6484`) - Add support for :attr:`MessageReference.fail_if_not_exists` (:issue:`6484`)
- By default, if the message you're replying to doesn't exist then the API errors out. - By default, if the message you're replying to doesn't exist then the API errors out.

View File

@ -61,7 +61,7 @@ async def cool(ctx):
In reality this just checks if a subcommand is being invoked. In reality this just checks if a subcommand is being invoked.
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send('No, {0.subcommand_passed} is not cool'.format(ctx)) await ctx.send(f'No, {ctx.subcommand_passed} is not cool')
@cool.command(name='bot') @cool.command(name='bot')
async def _bot(ctx): async def _bot(ctx):

View File

@ -70,9 +70,9 @@ class Music(commands.Cog):
"""Plays a file from the local filesystem""" """Plays a file from the local filesystem"""
source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query)) source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query))
ctx.voice_client.play(source, after=lambda e: print('Player error: %s' % e) if e else None) ctx.voice_client.play(source, after=lambda e: print(f'Player error: {e}') if e else None)
await ctx.send('Now playing: {}'.format(query)) await ctx.send(f'Now playing: {query}')
@commands.command() @commands.command()
async def yt(self, ctx, *, url): async def yt(self, ctx, *, url):
@ -80,9 +80,9 @@ class Music(commands.Cog):
async with ctx.typing(): async with ctx.typing():
player = await YTDLSource.from_url(url, loop=self.bot.loop) player = await YTDLSource.from_url(url, loop=self.bot.loop)
ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None) ctx.voice_client.play(player, after=lambda e: print(f'Player error: {e}') if e else None)
await ctx.send('Now playing: {}'.format(player.title)) await ctx.send(f'Now playing: {player.title}')
@commands.command() @commands.command()
async def stream(self, ctx, *, url): async def stream(self, ctx, *, url):
@ -90,9 +90,9 @@ class Music(commands.Cog):
async with ctx.typing(): async with ctx.typing():
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None) ctx.voice_client.play(player, after=lambda e: print(f'Player error: {e}') if e else None)
await ctx.send('Now playing: {}'.format(player.title)) await ctx.send(f'Now playing: {player.title}')
@commands.command() @commands.command()
async def volume(self, ctx, volume: int): async def volume(self, ctx, volume: int):
@ -102,7 +102,7 @@ class Music(commands.Cog):
return await ctx.send("Not connected to a voice channel.") return await ctx.send("Not connected to a voice channel.")
ctx.voice_client.source.volume = volume / 100 ctx.voice_client.source.volume = volume / 100
await ctx.send("Changed volume to {}%".format(volume)) await ctx.send(f"Changed volume to {volume}%")
@commands.command() @commands.command()
async def stop(self, ctx): async def stop(self, ctx):

View File

@ -29,7 +29,7 @@ async def userinfo(ctx: commands.Context, user: discord.User):
user_id = user.id user_id = user.id
username = user.name username = user.name
avatar = user.avatar_url avatar = user.avatar_url
await ctx.send('User found: {} -- {}\n{}'.format(user_id, username, avatar)) await ctx.send(f'User found: {user_id} -- {username}\n{avatar}')
@userinfo.error @userinfo.error
async def userinfo_error(ctx: commands.Context, error: commands.CommandError): async def userinfo_error(ctx: commands.Context, error: commands.CommandError):
@ -70,7 +70,7 @@ class ChannelOrMemberConverter(commands.Converter):
# If the value could not be converted we can raise an error # If the value could not be converted we can raise an error
# so our error handlers can deal with it in one place. # so our error handlers can deal with it in one place.
# The error has to be CommandError derived, so BadArgument works fine here. # The error has to be CommandError derived, so BadArgument works fine here.
raise commands.BadArgument('No Member or TextChannel could be converted from "{}"'.format(argument)) raise commands.BadArgument(f'No Member or TextChannel could be converted from "{argument}"')
@ -81,7 +81,7 @@ async def notify(ctx: commands.Context, target: ChannelOrMemberConverter):
# the `argument` parameter of the `ChannelOrMemberConverter.convert` method and # the `argument` parameter of the `ChannelOrMemberConverter.convert` method and
# the conversion will go through the process defined there. # the conversion will go through the process defined there.
await target.send('Hello, {}!'.format(target.name)) await target.send(f'Hello, {target.name}!')
@bot.command() @bot.command()
async def ignore(ctx: commands.Context, target: typing.Union[discord.Member, discord.TextChannel]): async def ignore(ctx: commands.Context, target: typing.Union[discord.Member, discord.TextChannel]):
@ -95,9 +95,9 @@ async def ignore(ctx: commands.Context, target: typing.Union[discord.Member, dis
# To check the resulting type, `isinstance` is used # To check the resulting type, `isinstance` is used
if isinstance(target, discord.Member): if isinstance(target, discord.Member):
await ctx.send('Member found: {}, adding them to the ignore list.'.format(target.mention)) await ctx.send(f'Member found: {target.mention}, adding them to the ignore list.')
elif isinstance(target, discord.TextChannel): # this could be an `else` but for completeness' sake. elif isinstance(target, discord.TextChannel): # this could be an `else` but for completeness' sake.
await ctx.send('Channel found: {}, adding it to the ignore list.'.format(target.mention)) await ctx.send(f'Channel found: {target.mention}, adding it to the ignore list.')
# Built-in type converters. # Built-in type converters.
@bot.command() @bot.command()

View File

@ -25,12 +25,12 @@ class MyClient(discord.Client):
try: try:
guess = await self.wait_for('message', check=is_correct, timeout=5.0) guess = await self.wait_for('message', check=is_correct, timeout=5.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await message.channel.send('Sorry, you took too long it was {}.'.format(answer)) return await message.channel.send(f'Sorry, you took too long it was {answer}.')
if int(guess.content) == answer: if int(guess.content) == answer:
await message.channel.send('You are right!') await message.channel.send('You are right!')
else: else:
await message.channel.send('Oops. It is actually {}.'.format(answer)) await message.channel.send(f'Oops. It is actually {answer}.')
client = MyClient() client = MyClient()
client.run('token') client.run('token')

View File

@ -12,7 +12,7 @@ class MyClient(discord.Client):
async def on_member_join(self, member): async def on_member_join(self, member):
guild = member.guild guild = member.guild
if guild.system_channel is not None: if guild.system_channel is not None:
to_send = 'Welcome {0.mention} to {1.name}!'.format(member, guild) to_send = f'Welcome {member.mention} to {guild.name}!'
await guild.system_channel.send(to_send) await guild.system_channel.send(to_send)

View File

@ -50,7 +50,7 @@ setup(name='enhanced-dpy',
"Issue tracker": "https://github.com/iDutchy/discord.py/issues", "Issue tracker": "https://github.com/iDutchy/discord.py/issues",
}, },
version=version, version=version,
packages=['discord', 'discord.ext.commands', 'discord.ext.tasks'], packages=['discord', 'discord.types', 'discord.ext.commands', 'discord.ext.tasks'],
license='MIT', license='MIT',
description='A Python wrapper for the Discord API', description='A Python wrapper for the Discord API',
long_description=readme, long_description=readme,
@ -58,17 +58,15 @@ setup(name='enhanced-dpy',
include_package_data=True, include_package_data=True,
install_requires=requirements, install_requires=requirements,
extras_require=extras_require, extras_require=extras_require,
python_requires='>=3.5.3', python_requires='>=3.8.0',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',