conflict fixes
This commit is contained in:
commit
a453266cd4
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,39 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report broken or incorrect behaviour
|
||||
labels: bug
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- A summary of your bug report -->
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
<!-- What you did to make it happen. Ideally there should be a short code snippet in this section to help reproduce the bug. -->
|
||||
|
||||
## Expected Results
|
||||
|
||||
<!-- What you expected to happen -->
|
||||
|
||||
## Actual Results
|
||||
|
||||
<!-- What actually happened. If there is a traceback, please show the entire thing. -->
|
||||
|
||||
## Intents
|
||||
|
||||
<!-- What intents are you using for your bot? This is the `discord.Intents` class you pass to the client -->
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Put an x inside [ ] to check it, like so: [x] -->
|
||||
|
||||
- [ ] I have searched the open issues for duplicates.
|
||||
- [ ] I have shown the entire traceback, if possible.
|
||||
- [ ] I have removed my token from display, if visible.
|
||||
- [ ] I have provided the intents that my bot is using.
|
||||
|
||||
## System Information
|
||||
|
||||
<!-- Run `python -m discord -v` and paste this information below. -->
|
||||
<!-- This command is available in v1.1.0 or higher. If you are unable to run this command, paste basic info (ie. Python version, library version, and your operating system -->
|
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
name: Bug Report
|
||||
description: Report broken or incorrect behaviour
|
||||
labels: unconfirmed bug
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for taking the time to fill out a bug.
|
||||
If you want real-time support, consider joining our Discord at https://discord.gg/r3sSKJJ instead.
|
||||
|
||||
Please note that this form is for bugs only!
|
||||
- type: input
|
||||
attributes:
|
||||
label: Summary
|
||||
description: A simple summary of your bug report
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: >
|
||||
What you did to make it happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal Reproducible Code
|
||||
description: >
|
||||
A short snippet of code that showcases the bug.
|
||||
render: python
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Results
|
||||
description: >
|
||||
What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Results
|
||||
description: >
|
||||
What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Intents
|
||||
description: >
|
||||
What intents are you using for your bot?
|
||||
This is the `discord.Intents` class you pass to the client.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: System Information
|
||||
description: >
|
||||
Run `python -m discord -v` and paste this information below.
|
||||
|
||||
This command required v1.1.0 or higher of the library. If this errors out then show some basic
|
||||
information involving your system such as operating system and Python version.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: >
|
||||
Let's make sure you've properly done due dilligence when reporting this issue!
|
||||
options:
|
||||
- label: I have searched the open issues for duplicates.
|
||||
required: true
|
||||
- label: I have shown the entire traceback, if possible.
|
||||
required: true
|
||||
- label: I have removed my token from display, if visible.
|
||||
required: true
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -4,5 +4,5 @@ contact_links:
|
||||
about: Ask questions and discuss with other users of the library.
|
||||
url: https://github.com/Rapptz/discord.py/discussions
|
||||
- name: Discord Server
|
||||
about: Use our official Discord server to ask help and questions as well.
|
||||
about: Use our official Discord server to ask for help and questions as well.
|
||||
url: https://discord.gg/r3sSKJJ
|
||||
|
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a feature for this library
|
||||
labels: feature request
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
<!--
|
||||
What problem is your feature trying to solve? What becomes easier or possible when this feature is implemented?
|
||||
-->
|
||||
|
||||
## The Ideal Solution
|
||||
|
||||
<!--
|
||||
What is your ideal solution to the problem? What would you like this feature to do?
|
||||
-->
|
||||
|
||||
## The Current Solution
|
||||
|
||||
<!--
|
||||
What is the current solution to the problem, if any?
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- A short summary of your feature request. -->
|
46
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
46
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
name: Feature Request
|
||||
description: Suggest a feature for this library
|
||||
labels: feature request
|
||||
issue_body: true
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Summary
|
||||
description: >
|
||||
A short summary of what your feature request is.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
multiple: false
|
||||
label: What is the feature request for?
|
||||
options:
|
||||
- The core library
|
||||
- discord.ext.commands
|
||||
- discord.ext.tasks
|
||||
- The documentation
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: The Problem
|
||||
description: >
|
||||
What problem is your feature trying to solve?
|
||||
What becomes easier or possible when this feature is implemented?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: The Ideal Solution
|
||||
description: >
|
||||
What is your ideal solution to the problem?
|
||||
What would you like this feature to do?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: The Current Solution
|
||||
description: >
|
||||
What is the current solution to the problem, if any?
|
||||
validations:
|
||||
required: false
|
@ -10,7 +10,7 @@ sphinx:
|
||||
builder: html
|
||||
|
||||
python:
|
||||
version: 3.7
|
||||
version: 3.8
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
@ -1,12 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Discord API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A basic wrapper for the Discord API.
|
||||
|
||||
:copyright: (c) 2015-2020 Rapptz
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
@ -14,54 +12,53 @@ A basic wrapper for the Discord API.
|
||||
__title__ = 'discord'
|
||||
__author__ = 'Rapptz'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2015-2020 Rapptz'
|
||||
__version__ = '1.6.0.7'
|
||||
__copyright__ = 'Copyright 2015-present Rapptz'
|
||||
__version__ = '2.0.0.7a'
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from .client import Client
|
||||
from .appinfo import AppInfo
|
||||
from .user import User, ClientUser, Profile
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .client import *
|
||||
from .appinfo import *
|
||||
from .user import *
|
||||
from .emoji import *
|
||||
from .partial_emoji import *
|
||||
from .activity import *
|
||||
from .channel import *
|
||||
from .guild import Guild
|
||||
from .guild import *
|
||||
from .flags import *
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .member import *
|
||||
from .message import *
|
||||
from .asset import Asset
|
||||
from .asset import *
|
||||
from .errors import *
|
||||
from .calls import CallMessage, GroupCall
|
||||
from .permissions import Permissions, PermissionOverwrite
|
||||
from .role import Role, RoleTags
|
||||
from .file import File
|
||||
from .colour import Color, Colour
|
||||
from .integrations import Integration, IntegrationAccount
|
||||
from .invite import Invite, PartialInviteChannel, PartialInviteGuild
|
||||
from .template import Template
|
||||
from .widget import Widget, WidgetMember, WidgetChannel
|
||||
from .object import Object
|
||||
from .reaction import Reaction
|
||||
from .permissions import *
|
||||
from .role import *
|
||||
from .file import *
|
||||
from .colour import *
|
||||
from .integrations import *
|
||||
from .invite import *
|
||||
from .template import *
|
||||
from .widget import *
|
||||
from .object import *
|
||||
from .reaction import *
|
||||
from . import utils, opus, abc
|
||||
from .enums import *
|
||||
from .embeds import Embed
|
||||
from .mentions import AllowedMentions
|
||||
from .shard import AutoShardedClient, ShardInfo
|
||||
from .embeds import *
|
||||
from .mentions import *
|
||||
from .shard import *
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
|
||||
from .voice_client import *
|
||||
from .audit_logs import *
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
from .sticker import Sticker
|
||||
from .sticker import *
|
||||
from .interactions import *
|
||||
|
||||
VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial')
|
||||
|
||||
version_info = VersionInfo(major=1, minor=6, micro=0, enhanced=7, releaselevel='alpha', serial=0)
|
||||
version_info = VersionInfo(major=2, minor=0, micro=0, enhanced=7, releaselevel='alpha', serial=0)
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -42,9 +40,9 @@ def show_version():
|
||||
if version_info.releaselevel != 'final':
|
||||
pkg = pkg_resources.get_distribution('discord.py')
|
||||
if pkg:
|
||||
entries.append(' - discord.py pkg_resources: v{0}'.format(pkg.version))
|
||||
entries.append(f' - discord.py pkg_resources: v{pkg.version}')
|
||||
|
||||
entries.append('- aiohttp v{0.__version__}'.format(aiohttp))
|
||||
entries.append(f'- aiohttp v{aiohttp.__version__}')
|
||||
uname = platform.uname()
|
||||
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
|
||||
print('\n'.join(entries))
|
||||
@ -54,7 +52,6 @@ def core(parser, args):
|
||||
show_version()
|
||||
|
||||
bot_template = """#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
@ -110,9 +107,7 @@ var/
|
||||
config.py
|
||||
"""
|
||||
|
||||
cog_template = '''# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
cog_template = '''from discord.ext import commands
|
||||
import discord
|
||||
|
||||
class {name}(commands.Cog{attrs}):
|
||||
@ -200,7 +195,7 @@ def newbot(parser, args):
|
||||
try:
|
||||
new_directory.mkdir(exist_ok=True, parents=True)
|
||||
except OSError as exc:
|
||||
parser.error('could not create our bot directory ({})'.format(exc))
|
||||
parser.error(f'could not create our bot directory ({exc})')
|
||||
|
||||
cogs = new_directory / 'cogs'
|
||||
|
||||
@ -209,27 +204,27 @@ def newbot(parser, args):
|
||||
init = cogs / '__init__.py'
|
||||
init.touch()
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(exc))
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
|
||||
fp.write('token = "place your token here"\ncogs = []\n')
|
||||
except OSError as exc:
|
||||
parser.error('could not create config file ({})'.format(exc))
|
||||
parser.error(f'could not create config file ({exc})')
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp:
|
||||
base = 'Bot' if not args.sharded else 'AutoShardedBot'
|
||||
fp.write(bot_template.format(base=base, prefix=args.prefix))
|
||||
except OSError as exc:
|
||||
parser.error('could not create bot file ({})'.format(exc))
|
||||
parser.error(f'could not create bot file ({exc})')
|
||||
|
||||
if not args.no_git:
|
||||
try:
|
||||
with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
|
||||
fp.write(gitignore_template)
|
||||
except OSError as exc:
|
||||
print('warning: could not create .gitignore file ({})'.format(exc))
|
||||
print(f'warning: could not create .gitignore file ({exc})')
|
||||
|
||||
print('successfully made bot at', new_directory)
|
||||
|
||||
@ -238,7 +233,7 @@ def newcog(parser, args):
|
||||
try:
|
||||
cog_dir.mkdir(exist_ok=True)
|
||||
except OSError as exc:
|
||||
print('warning: could not create cogs directory ({})'.format(exc))
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
|
||||
directory = cog_dir / to_path(parser, args.name)
|
||||
directory = directory.with_suffix('.py')
|
||||
@ -250,18 +245,19 @@ def newcog(parser, args):
|
||||
name = args.class_name
|
||||
else:
|
||||
name = str(directory.stem)
|
||||
if '-' in name:
|
||||
name = name.replace('-', ' ').title().replace(' ', '')
|
||||
if '-' in name or '_' in name:
|
||||
translation = str.maketrans('-_', ' ')
|
||||
name = name.translate(translation).title().replace(' ', '')
|
||||
else:
|
||||
name = name.title()
|
||||
|
||||
if args.display_name:
|
||||
attrs += ', name="{}"'.format(args.display_name)
|
||||
attrs += f', name="{args.display_name}"'
|
||||
if args.hide_commands:
|
||||
attrs += ', command_attrs=dict(hidden=True)'
|
||||
fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
|
||||
except OSError as exc:
|
||||
parser.error('could not create cog file ({})'.format(exc))
|
||||
parser.error(f'could not create cog file ({exc})')
|
||||
else:
|
||||
print('successfully made cog at', directory)
|
||||
|
||||
@ -300,4 +296,5 @@ def main():
|
||||
parser, args = parse_args()
|
||||
args.func(parser, args)
|
||||
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
280
discord/abc.py
280
discord/abc.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,15 +22,18 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import abc
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
||||
|
||||
from .iterators import HistoryIterator
|
||||
from .context_managers import Typing
|
||||
from .enums import ChannelType
|
||||
from .errors import InvalidArgument, ClientException
|
||||
from .mentions import AllowedMentions
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .role import Role
|
||||
from .invite import Invite
|
||||
@ -40,13 +41,31 @@ from .file import File
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'Snowflake',
|
||||
'User',
|
||||
'PrivateChannel',
|
||||
'GuildChannel',
|
||||
'Messageable',
|
||||
'Connectable',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from .user import ClientUser
|
||||
|
||||
|
||||
class _Undefined:
|
||||
def __repr__(self):
|
||||
return 'see-below'
|
||||
|
||||
|
||||
_undefined = _Undefined()
|
||||
|
||||
class Snowflake(metaclass=abc.ABCMeta):
|
||||
|
||||
@runtime_checkable
|
||||
class Snowflake(Protocol):
|
||||
"""An ABC that details the common operations on a Discord model.
|
||||
|
||||
Almost all :ref:`Discord models <discord_api_models>` meet this
|
||||
@ -61,27 +80,16 @@ class Snowflake(metaclass=abc.ABCMeta):
|
||||
The model's unique ID.
|
||||
"""
|
||||
__slots__ = ()
|
||||
id: int
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the model's creation time as a naive datetime in UTC."""
|
||||
def created_at(self) -> datetime:
|
||||
""":class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is Snowflake:
|
||||
mro = C.__mro__
|
||||
for attr in ('created_at', 'id'):
|
||||
for base in mro:
|
||||
if attr in base.__dict__:
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
return NotImplemented
|
||||
|
||||
class User(metaclass=abc.ABCMeta):
|
||||
@runtime_checkable
|
||||
class User(Snowflake, Protocol):
|
||||
"""An ABC that details the common operations on a Discord user.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -105,35 +113,24 @@ class User(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
name: str
|
||||
discriminator: str
|
||||
avatar: Optional[str]
|
||||
bot: bool
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def display_name(self):
|
||||
def display_name(self) -> str:
|
||||
""":class:`str`: Returns the user's display name."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mention(self):
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is User:
|
||||
if Snowflake.__subclasshook__(C) is NotImplemented:
|
||||
return NotImplemented
|
||||
|
||||
mro = C.__mro__
|
||||
for attr in ('display_name', 'mention', 'name', 'avatar', 'discriminator', 'bot'):
|
||||
for base in mro:
|
||||
if attr in base.__dict__:
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
return NotImplemented
|
||||
|
||||
class PrivateChannel(metaclass=abc.ABCMeta):
|
||||
@runtime_checkable
|
||||
class PrivateChannel(Snowflake, Protocol):
|
||||
"""An ABC that details the common operations on a private Discord channel.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -150,37 +147,28 @@ class PrivateChannel(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is PrivateChannel:
|
||||
if Snowflake.__subclasshook__(C) is NotImplemented:
|
||||
return NotImplemented
|
||||
me: ClientUser
|
||||
|
||||
mro = C.__mro__
|
||||
for base in mro:
|
||||
if 'me' in base.__dict__:
|
||||
return True
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
class _Overwrites:
|
||||
__slots__ = ('id', 'allow', 'deny', 'type')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.allow = kwargs.pop('allow', 0)
|
||||
self.deny = kwargs.pop('deny', 0)
|
||||
self.allow = int(kwargs.pop('allow_new', 0))
|
||||
self.deny = int(kwargs.pop('deny_new', 0))
|
||||
self.type = sys.intern(kwargs.pop('type'))
|
||||
|
||||
def _asdict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'allow': self.allow,
|
||||
'deny': self.deny,
|
||||
'allow': str(self.allow),
|
||||
'deny': str(self.deny),
|
||||
'type': self.type,
|
||||
}
|
||||
|
||||
class GuildChannel:
|
||||
|
||||
class GuildChannel(Protocol):
|
||||
"""An ABC that details the common operations on a Discord guild channel.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -191,6 +179,11 @@ class GuildChannel:
|
||||
|
||||
This ABC must also implement :class:`~discord.abc.Snowflake`.
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -259,6 +252,13 @@ class GuildChannel:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
rtc_region = options.pop('rtc_region')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
||||
|
||||
lock_permissions = options.pop('sync_permissions', False)
|
||||
|
||||
try:
|
||||
@ -356,7 +356,7 @@ class GuildChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -710,6 +710,126 @@ class GuildChannel:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def move(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
A rich interface to help move a channel relative to other channels.
|
||||
|
||||
If exact position movement is required, :meth:`edit` should be used instead.
|
||||
|
||||
You must have the :attr:`~discord.Permissions.manage_channels` permission to
|
||||
do this.
|
||||
|
||||
.. note::
|
||||
|
||||
Voice channels will always be sorted below text channels.
|
||||
This is a Discord limitation.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
------------
|
||||
beginning: :class:`bool`
|
||||
Whether to move the channel to the beginning of the
|
||||
channel list (or category if given).
|
||||
This is mutually exclusive with ``end``, ``before``, and ``after``.
|
||||
end: :class:`bool`
|
||||
Whether to move the channel to the end of the
|
||||
channel list (or category if given).
|
||||
This is mutually exclusive with ``beginning``, ``before``, and ``after``.
|
||||
before: :class:`abc.Snowflake`
|
||||
The channel that should be before our current channel.
|
||||
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
|
||||
after: :class:`abc.Snowflake`
|
||||
The channel that should be after our current channel.
|
||||
This is mutually exclusive with ``beginning``, ``end``, and ``before``.
|
||||
offset: :class:`int`
|
||||
The number of channels to offset the move by. For example,
|
||||
an offset of ``2`` with ``beginning=True`` would move
|
||||
it 2 after the beginning. A positive number moves it below
|
||||
while a negative number moves it above. Note that this
|
||||
number is relative and computed after the ``beginning``,
|
||||
``end``, ``before``, and ``after`` parameters.
|
||||
category: Optional[:class:`abc.Snowflake`]
|
||||
The category to move this channel under.
|
||||
If ``None`` is given then it moves it out of the category.
|
||||
This parameter is ignored if moving a category channel.
|
||||
sync_permissions: :class:`bool`
|
||||
Whether to sync the permissions with the category (if given).
|
||||
reason: :class:`str`
|
||||
The reason for the move.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
An invalid position was given or a bad mix of arguments were passed.
|
||||
Forbidden
|
||||
You do not have permissions to move the channel.
|
||||
HTTPException
|
||||
Moving the channel failed.
|
||||
"""
|
||||
|
||||
if not kwargs:
|
||||
return
|
||||
|
||||
beginning, end = kwargs.get('beginning'), kwargs.get('end')
|
||||
before, after = kwargs.get('before'), kwargs.get('after')
|
||||
offset = kwargs.get('offset', 0)
|
||||
if sum(bool(a) for a in (beginning, end, before, after)) > 1:
|
||||
raise InvalidArgument('Only one of [before, after, end, beginning] can be used.')
|
||||
|
||||
bucket = self._sorting_bucket
|
||||
parent_id = kwargs.get('category', ...)
|
||||
if parent_id not in (..., None):
|
||||
parent_id = parent_id.id
|
||||
channels = [
|
||||
ch
|
||||
for ch in self.guild.channels
|
||||
if ch._sorting_bucket == bucket
|
||||
and ch.category_id == parent_id
|
||||
]
|
||||
else:
|
||||
channels = [
|
||||
ch
|
||||
for ch in self.guild.channels
|
||||
if ch._sorting_bucket == bucket
|
||||
and ch.category_id == self.category_id
|
||||
]
|
||||
|
||||
channels.sort(key=lambda c: (c.position, c.id))
|
||||
|
||||
try:
|
||||
# Try to remove ourselves from the channel list
|
||||
channels.remove(self)
|
||||
except ValueError:
|
||||
# If we're not there then it's probably due to not being in the category
|
||||
pass
|
||||
|
||||
index = None
|
||||
if beginning:
|
||||
index = 0
|
||||
elif end:
|
||||
index = len(channels)
|
||||
elif before:
|
||||
index = next((i for i, c in enumerate(channels) if c.id == before.id), None)
|
||||
elif after:
|
||||
index = next((i + 1 for i, c in enumerate(channels) if c.id == after.id), None)
|
||||
|
||||
if index is None:
|
||||
raise InvalidArgument('Could not resolve appropriate move position')
|
||||
|
||||
channels.insert(max((index + offset), 0), self)
|
||||
payload = []
|
||||
lock_permissions = kwargs.get('sync_permissions', False)
|
||||
reason = kwargs.get('reason')
|
||||
for index, channel in enumerate(channels):
|
||||
d = {'id': channel.id, 'position': index}
|
||||
if parent_id is not ... and channel.id == self.id:
|
||||
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
|
||||
payload.append(d)
|
||||
|
||||
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
|
||||
|
||||
async def create_invite(self, *, reason=None, **fields):
|
||||
"""|coro|
|
||||
|
||||
@ -784,7 +904,8 @@ class GuildChannel:
|
||||
|
||||
return result
|
||||
|
||||
class Messageable(metaclass=abc.ABCMeta):
|
||||
|
||||
class Messageable(Protocol):
|
||||
"""An ABC that details the common operations on a model that can send messages.
|
||||
|
||||
The following implement this ABC:
|
||||
@ -795,11 +916,16 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
- :class:`~discord.User`
|
||||
- :class:`~discord.Member`
|
||||
- :class:`~discord.ext.commands.Context`
|
||||
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _get_channel(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -904,7 +1030,7 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
allowed_mentions = state.allowed_mentions and state.allowed_mentions.to_dict()
|
||||
|
||||
if mention_author is not None:
|
||||
allowed_mentions = allowed_mentions or {}
|
||||
allowed_mentions = allowed_mentions or AllowedMentions().to_dict()
|
||||
allowed_mentions['replied_user'] = bool(mention_author)
|
||||
|
||||
if reference is not None:
|
||||
@ -942,8 +1068,8 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
f.close()
|
||||
else:
|
||||
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
|
||||
nonce=nonce, allowed_mentions=allowed_mentions,
|
||||
message_reference=reference)
|
||||
nonce=nonce, allowed_mentions=allowed_mentions,
|
||||
message_reference=reference)
|
||||
|
||||
ret = state.create_message(channel=channel, data=data)
|
||||
if delete_after is not None:
|
||||
@ -1068,13 +1194,16 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
that this would make it a slow operation.
|
||||
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages before this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages after this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve messages around this date or message.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
When using this argument, the maximum limit is 101. Note that if the limit is an
|
||||
even number then this will return at most limit + 1 messages.
|
||||
oldest_first: Optional[:class:`bool`]
|
||||
@ -1095,21 +1224,25 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
|
||||
|
||||
class Connectable(metaclass=abc.ABCMeta):
|
||||
|
||||
class Connectable(Protocol):
|
||||
"""An ABC that details the common operations on a channel that can
|
||||
connect to a voice server.
|
||||
|
||||
The following implement this ABC:
|
||||
|
||||
- :class:`~discord.VoiceChannel`
|
||||
|
||||
Note
|
||||
----
|
||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||
checks.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_voice_client_key(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_voice_state_pair(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -1146,9 +1279,6 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
A voice client that is fully connected to the voice server.
|
||||
"""
|
||||
|
||||
if not issubclass(cls, VoiceProtocol):
|
||||
raise TypeError('Type must meet VoiceProtocol abstract base class.')
|
||||
|
||||
key_id, _ = self._get_voice_client_key()
|
||||
state = self._state
|
||||
|
||||
@ -1157,6 +1287,10 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
|
||||
client = state._get_client()
|
||||
voice = cls(client, self)
|
||||
|
||||
if not isinstance(voice, VoiceProtocol):
|
||||
raise TypeError('Type must meet VoiceProtocol abstract base class.')
|
||||
|
||||
state._add_voice_client(key_id, voice)
|
||||
|
||||
try:
|
||||
@ -1167,6 +1301,6 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
except Exception:
|
||||
# we don't care if disconnect failed because connection failed
|
||||
pass
|
||||
raise # re-raise
|
||||
raise # re-raise
|
||||
|
||||
return voice
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -187,7 +185,10 @@ class Activity(BaseActivity):
|
||||
self.flags = kwargs.pop('flags', 0)
|
||||
self.sync_id = kwargs.pop('sync_id', None)
|
||||
self.session_id = kwargs.pop('session_id', None)
|
||||
self.type = try_enum(ActivityType, kwargs.pop('type', -1))
|
||||
|
||||
activity_type = kwargs.pop('type', -1)
|
||||
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
|
||||
|
||||
emoji = kwargs.pop('emoji', None)
|
||||
if emoji is not None:
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
@ -196,16 +197,16 @@ class Activity(BaseActivity):
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
'type',
|
||||
'name',
|
||||
'url',
|
||||
'details',
|
||||
'application_id',
|
||||
'session_id',
|
||||
'emoji',
|
||||
('type', self.type),
|
||||
('name', self.name),
|
||||
('url', self.url),
|
||||
('details', self.details),
|
||||
('application_id', self.application_id),
|
||||
('session_id', self.session_id),
|
||||
('emoji', self.emoji),
|
||||
)
|
||||
mapped = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in attrs)
|
||||
return '<Activity %s>' % mapped
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<Activity {inner}>'
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
@ -227,17 +228,21 @@ class Activity(BaseActivity):
|
||||
def start(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps['start'] / 1000)
|
||||
timestamp = self.timestamps['start'] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps['end'] / 1000)
|
||||
timestamp = self.timestamps['end'] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def large_image_url(self):
|
||||
@ -250,7 +255,7 @@ class Activity(BaseActivity):
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, large_image)
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{large_image}.png'
|
||||
|
||||
@property
|
||||
def small_image_url(self):
|
||||
@ -263,7 +268,7 @@ class Activity(BaseActivity):
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + '/app-assets/{0}/{1}.png'.format(self.application_id, small_image)
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
|
||||
@property
|
||||
def large_image_text(self):
|
||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
||||
@ -302,10 +307,6 @@ class Game(BaseActivity):
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The game's name.
|
||||
start: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
|
||||
end: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
@ -322,20 +323,12 @@ class Game(BaseActivity):
|
||||
try:
|
||||
timestamps = extra['timestamps']
|
||||
except KeyError:
|
||||
self._extract_timestamp(extra, 'start')
|
||||
self._extract_timestamp(extra, 'end')
|
||||
self._start = 0
|
||||
self._end = 0
|
||||
else:
|
||||
self._start = timestamps.get('start', 0)
|
||||
self._end = timestamps.get('end', 0)
|
||||
|
||||
def _extract_timestamp(self, data, key):
|
||||
try:
|
||||
dt = data[key]
|
||||
except KeyError:
|
||||
setattr(self, '_' + key, 0)
|
||||
else:
|
||||
setattr(self, '_' + key, dt.timestamp() * 1000.0)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
@ -348,21 +341,21 @@ class Game(BaseActivity):
|
||||
def start(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
|
||||
if self._start:
|
||||
return datetime.datetime.utcfromtimestamp(self._start / 1000)
|
||||
return datetime.datetime.utcfromtimestamp(self._start / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
|
||||
if self._end:
|
||||
return datetime.datetime.utcfromtimestamp(self._end / 1000)
|
||||
return datetime.datetime.utcfromtimestamp(self._end / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Game name={0.name!r}>'.format(self)
|
||||
return f'<Game name={self.name!r}>'
|
||||
|
||||
def to_dict(self):
|
||||
timestamps = {}
|
||||
@ -455,7 +448,7 @@ class Streaming(BaseActivity):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Streaming name={0.name!r}>'.format(self)
|
||||
return f'<Streaming name={self.name!r}>'
|
||||
|
||||
@property
|
||||
def twitch_name(self):
|
||||
@ -693,8 +686,14 @@ class CustomActivity(BaseActivity):
|
||||
|
||||
if emoji is None:
|
||||
self.emoji = emoji
|
||||
else:
|
||||
elif isinstance(emoji, dict):
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
elif isinstance(emoji, str):
|
||||
self.emoji = PartialEmoji(name=emoji)
|
||||
elif isinstance(emoji, PartialEmoji):
|
||||
self.emoji = emoji
|
||||
else:
|
||||
raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
@ -733,7 +732,7 @@ class CustomActivity(BaseActivity):
|
||||
def __str__(self):
|
||||
if self.emoji:
|
||||
if self.name:
|
||||
return '%s %s' % (self.emoji, self.name)
|
||||
return f'{self.emoji} {self.name}'
|
||||
return str(self.emoji)
|
||||
else:
|
||||
return str(self.name)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,9 @@ from .user import User
|
||||
from .asset import Asset
|
||||
from .team import Team
|
||||
|
||||
__all__ = (
|
||||
'AppInfo',
|
||||
)
|
||||
|
||||
class AppInfo:
|
||||
"""Represents the application info for the bot provided by Discord.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,10 @@ from .errors import DiscordException
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'Asset',
|
||||
)
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
@ -74,11 +76,11 @@ class Asset:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not user.is_avatar_animated():
|
||||
raise InvalidArgument("non animated avatars do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if user.avatar is None:
|
||||
return user.default_avatar_url
|
||||
@ -96,7 +98,7 @@ class Asset:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
|
||||
return cls(state, url)
|
||||
@ -109,7 +111,7 @@ class Asset:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
|
||||
return cls(state, url)
|
||||
@ -119,7 +121,7 @@ class Asset:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if hash is None:
|
||||
return cls(state)
|
||||
@ -132,11 +134,11 @@ class Asset:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_AVATAR_FORMATS))
|
||||
raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not guild.is_icon_animated():
|
||||
raise InvalidArgument("non animated guild icons do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if guild.icon is None:
|
||||
return cls(state)
|
||||
@ -156,16 +158,16 @@ class Asset:
|
||||
@classmethod
|
||||
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not emoji.animated:
|
||||
raise InvalidArgument("non animated emoji's do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
if format is None:
|
||||
format = 'gif' if emoji.animated else static_format
|
||||
|
||||
return cls(state, '/emojis/{0.id}.{1}'.format(emoji, format))
|
||||
|
||||
return cls(state, f'/emojis/{emoji.id}.{format}')
|
||||
|
||||
def __str__(self):
|
||||
return self.BASE + self._url if self._url is not None else ''
|
||||
|
||||
@ -178,7 +180,7 @@ class Asset:
|
||||
return self._url is not None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Asset url={0._url!r}>'.format(self)
|
||||
return f'<Asset url={self._url!r}>'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,13 @@ from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
|
||||
__all__ = (
|
||||
'AuditLogDiff',
|
||||
'AuditLogChanges',
|
||||
'AuditLogEntry',
|
||||
)
|
||||
|
||||
def _transform_verification_level(entry, data):
|
||||
return enums.try_enum(enums.VerificationLevel, data)
|
||||
@ -51,8 +56,7 @@ def _transform_snowflake(entry, data):
|
||||
def _transform_channel(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
channel = entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
return channel
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
def _transform_owner_id(entry, data):
|
||||
if data is None:
|
||||
@ -94,7 +98,7 @@ class AuditLogDiff:
|
||||
|
||||
def __repr__(self):
|
||||
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
||||
return '<AuditLogDiff %s>' % values
|
||||
return f'<AuditLogDiff {values}>'
|
||||
|
||||
class AuditLogChanges:
|
||||
TRANSFORMERS = {
|
||||
@ -166,7 +170,7 @@ class AuditLogChanges:
|
||||
self.before.color = self.before.colour
|
||||
|
||||
def __repr__(self):
|
||||
return '<AuditLogChanges before=%r after=%r>' % (self.before, self.after)
|
||||
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
|
||||
|
||||
def _handle_role(self, first, second, entry, elem):
|
||||
if not hasattr(first, 'roles'):
|
||||
@ -187,11 +191,28 @@ class AuditLogChanges:
|
||||
|
||||
setattr(second, 'roles', data)
|
||||
|
||||
class AuditLogEntry:
|
||||
class AuditLogEntry(Hashable):
|
||||
r"""Represents an Audit Log entry.
|
||||
|
||||
You retrieve these via :meth:`Guild.audit_logs`.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two entries are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two entries are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the entry's hash.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Audit log entries are now comparable and hashable.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
action: :class:`AuditLogAction`
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
import time
|
||||
import random
|
||||
|
||||
__all__ = (
|
||||
'ExponentialBackoff',
|
||||
)
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""An implementation of the exponential backoff algorithm
|
||||
|
||||
|
155
discord/calls.py
155
discord/calls.py
@ -1,155 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from . import utils
|
||||
from .enums import VoiceRegion, try_enum
|
||||
from .member import VoiceState
|
||||
|
||||
class CallMessage:
|
||||
"""Represents a group call message from Discord.
|
||||
|
||||
This is only received in cases where the message type is equivalent to
|
||||
:attr:`MessageType.call`.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
ended_timestamp: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC datetime object that represents the time that the call has ended.
|
||||
participants: List[:class:`User`]
|
||||
The list of users that are participating in this call.
|
||||
message: :class:`Message`
|
||||
The message associated with this call message.
|
||||
"""
|
||||
|
||||
def __init__(self, message, **kwargs):
|
||||
self.message = message
|
||||
self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp'))
|
||||
self.participants = kwargs.get('participants')
|
||||
|
||||
@property
|
||||
def call_ended(self):
|
||||
""":class:`bool`: Indicates if the call has ended."""
|
||||
return self.ended_timestamp is not None
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: The private channel associated with this message."""
|
||||
return self.message.channel
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Queries the duration of the call.
|
||||
|
||||
If the call has not ended then the current duration will
|
||||
be returned.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`datetime.timedelta`
|
||||
The timedelta object representing the duration.
|
||||
"""
|
||||
if self.ended_timestamp is None:
|
||||
return datetime.datetime.utcnow() - self.message.created_at
|
||||
else:
|
||||
return self.ended_timestamp - self.message.created_at
|
||||
|
||||
class GroupCall:
|
||||
"""Represents the actual group call from Discord.
|
||||
|
||||
This is accompanied with a :class:`CallMessage` denoting the information.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
call: :class:`CallMessage`
|
||||
The call message associated with this group call.
|
||||
unavailable: :class:`bool`
|
||||
Denotes if this group call is unavailable.
|
||||
ringing: List[:class:`User`]
|
||||
A list of users that are currently being rung to join the call.
|
||||
region: :class:`VoiceRegion`
|
||||
The guild region the group call is being hosted on.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.call = kwargs.get('call')
|
||||
self.unavailable = kwargs.get('unavailable')
|
||||
self._voice_states = {}
|
||||
|
||||
for state in kwargs.get('voice_states', []):
|
||||
self._update_voice_state(state)
|
||||
|
||||
self._update(**kwargs)
|
||||
|
||||
def _update(self, **kwargs):
|
||||
self.region = try_enum(VoiceRegion, kwargs.get('region'))
|
||||
lookup = {u.id: u for u in self.call.channel.recipients}
|
||||
me = self.call.channel.me
|
||||
lookup[me.id] = me
|
||||
self.ringing = list(filter(None, map(lookup.get, kwargs.get('ringing', []))))
|
||||
|
||||
def _update_voice_state(self, data):
|
||||
user_id = int(data['user_id'])
|
||||
# left the voice channel?
|
||||
if data['channel_id'] is None:
|
||||
self._voice_states.pop(user_id, None)
|
||||
else:
|
||||
self._voice_states[user_id] = VoiceState(data=data, channel=self.channel)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""List[:class:`User`]: A property that returns all users that are currently in this call."""
|
||||
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
|
||||
me = self.channel.me
|
||||
if self.voice_state_for(me) is not None:
|
||||
ret.append(me)
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: Returns the channel the group call is in."""
|
||||
return self.call.channel
|
||||
|
||||
def voice_state_for(self, user):
|
||||
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
|
||||
|
||||
If the :class:`User` has no voice state then this function returns
|
||||
``None``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
user: :class:`User`
|
||||
The user to retrieve the voice state for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`VoiceState`]
|
||||
The voice state associated with this user.
|
||||
"""
|
||||
|
||||
return self._voice_states.get(user.id)
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,7 +27,7 @@ import asyncio
|
||||
|
||||
import discord.abc
|
||||
from .permissions import Permissions
|
||||
from .enums import ChannelType, try_enum
|
||||
from .enums import ChannelType, try_enum, VoiceRegion
|
||||
from .mixins import Hashable
|
||||
from . import utils
|
||||
from .asset import Asset
|
||||
@ -38,6 +36,7 @@ from .errors import ClientException, NoMoreItems, InvalidArgument
|
||||
__all__ = (
|
||||
'TextChannel',
|
||||
'VoiceChannel',
|
||||
'StageChannel',
|
||||
'DMChannel',
|
||||
'CategoryChannel',
|
||||
'StoreChannel',
|
||||
@ -114,7 +113,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
('news', self.is_news()),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
@ -148,6 +148,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
.. versionadded:: 1.5.0.2"""
|
||||
return self.permissions_for(self.guild.me).send_messages
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
@ -156,8 +157,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
||||
@ -247,6 +246,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
"""
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'topic': self.topic,
|
||||
@ -254,8 +254,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
'rate_limit_per_user': self.slowmode_delay
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def delete_messages(self, messages):
|
||||
"""|coro|
|
||||
|
||||
@ -320,10 +318,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
account). The :attr:`~Permissions.read_message_history` permission is
|
||||
also needed to retrieve message history.
|
||||
|
||||
Internally, this employs a different number of strategies depending
|
||||
on the conditions met such as if a bulk delete is possible or if
|
||||
the account is a user bot or not.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
@ -354,8 +348,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
bulk: :class:`bool`
|
||||
If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting
|
||||
a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will
|
||||
fall back to single delete if current account is a user bot, or if messages are
|
||||
older than two weeks.
|
||||
fall back to single delete if messages are older than two weeks.
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -378,7 +371,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
count = 0
|
||||
|
||||
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
|
||||
strategy = self.delete_messages if self._state.is_bot and bulk else _single_delete_strategy
|
||||
strategy = self.delete_messages if bulk else _single_delete_strategy
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -546,7 +539,80 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
from .message import PartialMessage
|
||||
return PartialMessage(channel=self, id=message_id)
|
||||
|
||||
class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
||||
'_state', 'position', '_overwrites', 'category_id',
|
||||
'rtc_region')
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self._update(guild, data)
|
||||
|
||||
def _get_voice_client_key(self):
|
||||
return self.guild.id, 'guild_id'
|
||||
|
||||
def _get_voice_state_pair(self):
|
||||
return self.guild.id, self.id
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data['name']
|
||||
self.rtc_region = data.get('rtc_region')
|
||||
if self.rtc_region:
|
||||
self.rtc_region = try_enum(VoiceRegion, self.rtc_region)
|
||||
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
||||
self.position = data['position']
|
||||
self.bitrate = data.get('bitrate')
|
||||
self.user_limit = data.get('user_limit')
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
def _sorting_bucket(self):
|
||||
return ChannelType.voice.value
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
||||
ret = []
|
||||
for user_id, state in self.guild._voice_states.items():
|
||||
if state.channel and state.channel.id == self.id:
|
||||
member = self.guild.get_member(user_id)
|
||||
if member is not None:
|
||||
ret.append(member)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def voice_states(self):
|
||||
"""Returns a mapping of member IDs who have voice states in this channel.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
.. note::
|
||||
|
||||
This function is intentionally low level to replace :attr:`members`
|
||||
when the member cache is unavailable.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Mapping[:class:`int`, :class:`VoiceState`]
|
||||
The mapping of member ID to a voice state.
|
||||
"""
|
||||
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# voice channels cannot be edited by people who can't connect to them
|
||||
# It also implicitly denies all other voice perms
|
||||
if not base.connect:
|
||||
denied = Permissions.voice()
|
||||
denied.update(manage_channels=True, manage_roles=True)
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
class VoiceChannel(VocalGuildChannel):
|
||||
"""Represents a Discord guild voice channel.
|
||||
|
||||
.. container:: operations
|
||||
@ -584,101 +650,40 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a voice channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
||||
'_state', 'position', '_overwrites', 'category_id')
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self._update(guild, data)
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
attrs = [
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('rtc_region', self.rtc_region),
|
||||
('position', self.position),
|
||||
('bitrate', self.bitrate),
|
||||
('user_limit', self.user_limit),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
|
||||
|
||||
def _get_voice_client_key(self):
|
||||
return self.guild.id, 'guild_id'
|
||||
|
||||
def _get_voice_state_pair(self):
|
||||
return self.guild.id, self.id
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.voice
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data['name']
|
||||
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
||||
self.position = data['position']
|
||||
self.bitrate = data.get('bitrate')
|
||||
self.user_limit = data.get('user_limit')
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
def _sorting_bucket(self):
|
||||
return ChannelType.voice.value
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
||||
ret = []
|
||||
for user_id, state in self.guild._voice_states.items():
|
||||
if state.channel.id == self.id:
|
||||
member = self.guild.get_member(user_id)
|
||||
if member is not None:
|
||||
ret.append(member)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def voice_states(self):
|
||||
"""Returns a mapping of member IDs who have voice states in this channel.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
.. note::
|
||||
|
||||
This function is intentionally low level to replace :attr:`members`
|
||||
when the member cache is unavailable.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Mapping[:class:`int`, :class:`VoiceState`]
|
||||
The mapping of member ID to a voice state.
|
||||
"""
|
||||
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
|
||||
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# voice channels cannot be edited by people who can't connect to them
|
||||
# It also implicitly denies all other voice perms
|
||||
if not base.connect:
|
||||
denied = Permissions.voice()
|
||||
denied.update(manage_channels=True, manage_roles=True)
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'bitrate': self.bitrate,
|
||||
'user_limit': self.user_limit
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -711,6 +716,136 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
overwrites: :class:`dict`
|
||||
A :class:`dict` of target (either a role or a member) to
|
||||
:class:`PermissionOverwrite` to apply to the channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The new region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If the permission overwrite information is not in proper form.
|
||||
Forbidden
|
||||
You do not have permissions to edit the channel.
|
||||
HTTPException
|
||||
Editing the channel failed.
|
||||
"""
|
||||
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
class StageChannel(VocalGuildChannel):
|
||||
"""Represents a Discord guild stage channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the channel's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The channel name.
|
||||
guild: :class:`Guild`
|
||||
The guild the channel belongs to.
|
||||
id: :class:`int`
|
||||
The channel ID.
|
||||
topic: Optional[:class:`str`]
|
||||
The channel's topic. ``None`` if it isn't set.
|
||||
category_id: Optional[:class:`int`]
|
||||
The category channel ID this channel belongs to, if applicable.
|
||||
position: :class:`int`
|
||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||
top channel is position 0.
|
||||
bitrate: :class:`int`
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a stage channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the stage channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
"""
|
||||
__slots__ = ('topic',)
|
||||
|
||||
def __repr__(self):
|
||||
attrs = [
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('topic', self.topic),
|
||||
('rtc_region', self.rtc_region),
|
||||
('position', self.position),
|
||||
('bitrate', self.bitrate),
|
||||
('user_limit', self.user_limit),
|
||||
('category_id', self.category_id)
|
||||
]
|
||||
joined = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {joined}>'
|
||||
|
||||
def _update(self, guild, data):
|
||||
super()._update(guild, data)
|
||||
self.topic = data.get('topic')
|
||||
|
||||
@property
|
||||
def requesting_to_speak(self):
|
||||
"""List[:class:`Member`]: A list of members who are requesting to speak in the stage channel."""
|
||||
return [member for member in self.members if member.voice.requested_to_speak_at is not None]
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.stage_voice
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'topic': self.topic,
|
||||
}, name=name, reason=reason)
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
Edits the channel.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: :class:`str`
|
||||
The new channel's name.
|
||||
topic: :class:`str`
|
||||
The new channel's topic.
|
||||
position: :class:`int`
|
||||
The new channel's position.
|
||||
sync_permissions: :class:`bool`
|
||||
Whether to sync permissions with the channel's new or pre-existing
|
||||
category. Defaults to ``False``.
|
||||
category: Optional[:class:`CategoryChannel`]
|
||||
The new category for this channel. Can be ``None`` to remove the
|
||||
category.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this channel. Shows up on the audit log.
|
||||
overwrites: :class:`dict`
|
||||
A :class:`dict` of target (either a role or a member) to
|
||||
:class:`PermissionOverwrite` to apply to the channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The new region for the stage channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -791,13 +926,12 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
""":class:`bool`: Checks if the category is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'nsfw': self.nsfw
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -835,6 +969,11 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
|
||||
await self._edit(options=options, reason=reason)
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.move)
|
||||
async def move(self, **kwargs):
|
||||
kwargs.pop('category', None)
|
||||
await super().move(**kwargs)
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
"""List[:class:`abc.GuildChannel`]: Returns the channels that are under this category.
|
||||
@ -866,6 +1005,18 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
ret.sort(key=lambda c: (c.position, c.id))
|
||||
return ret
|
||||
|
||||
@property
|
||||
def stage_channels(self):
|
||||
"""List[:class:`StageChannel`]: Returns the voice channels that are under this category.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
ret = [c for c in self.guild.channels
|
||||
if c.category_id == self.id
|
||||
and isinstance(c, StageChannel)]
|
||||
ret.sort(key=lambda c: (c.position, c.id))
|
||||
return ret
|
||||
|
||||
async def create_text_channel(self, name, *, overwrites=None, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -890,6 +1041,20 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""
|
||||
return await self.guild.create_voice_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
|
||||
|
||||
async def create_stage_channel(self, name, *, overwrites=None, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`StageChannel`
|
||||
The channel that was just created.
|
||||
"""
|
||||
return await self.guild.create_stage_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
|
||||
|
||||
class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord guild store channel.
|
||||
|
||||
@ -953,6 +1118,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return ChannelType.store
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
@ -961,19 +1127,16 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
def is_nsfw(self):
|
||||
""":class:`bool`: Checks if the channel is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
return await self._clone_impl({
|
||||
'nsfw': self.nsfw
|
||||
}, name=name, reason=reason)
|
||||
|
||||
clone.__doc__ = discord.abc.GuildChannel.clone.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
@ -1059,7 +1222,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return 'Direct Message with %s' % self.recipient
|
||||
return f'Direct Message with {self.recipient}'
|
||||
|
||||
def __repr__(self):
|
||||
return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self)
|
||||
@ -1255,8 +1418,8 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
This returns all the Text related permissions set to ``True`` except:
|
||||
|
||||
- send_tts_messages: You cannot send TTS messages in a DM.
|
||||
- manage_messages: You cannot delete others messages in a DM.
|
||||
- :attr:`~Permissions.send_tts_messages`: You cannot send TTS messages in a DM.
|
||||
- :attr:`~Permissions.manage_messages`: You cannot delete others messages in a DM.
|
||||
|
||||
This also checks the kick_members permission if the user is the owner.
|
||||
|
||||
@ -1281,86 +1444,6 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
return base
|
||||
|
||||
async def add_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Adds recipients to this group.
|
||||
|
||||
A group can only have a maximum of 10 members.
|
||||
Attempting to add more ends up in an exception. To
|
||||
add a recipient to the group, you must have a relationship
|
||||
with the user of type :attr:`RelationshipType.friend`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to add to this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Adding a recipient to this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.add_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def remove_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Removes recipients from this group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to remove from this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Removing a recipient from this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.remove_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[:class:`str`]
|
||||
The new name to change the group to.
|
||||
Could be ``None`` to remove the name.
|
||||
icon: Optional[:class:`bytes`]
|
||||
A :term:`py:bytes-like object` representing the new icon.
|
||||
Could be ``None`` to remove the icon.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the group failed.
|
||||
"""
|
||||
|
||||
try:
|
||||
icon_bytes = fields['icon']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if icon_bytes is not None:
|
||||
fields['icon'] = utils._bytes_to_base64_data(icon_bytes)
|
||||
|
||||
data = await self._state.http.edit_group(self.id, **fields)
|
||||
self._update_group(data)
|
||||
|
||||
async def leave(self):
|
||||
"""|coro|
|
||||
|
||||
@ -1392,5 +1475,7 @@ def _channel_factory(channel_type):
|
||||
return TextChannel, value
|
||||
elif value is ChannelType.store:
|
||||
return StoreChannel, value
|
||||
elif value is ChannelType.stage_voice:
|
||||
return StageChannel, value
|
||||
else:
|
||||
return None, value
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -34,7 +32,7 @@ import re
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .user import User, Profile
|
||||
from .user import User
|
||||
from .invite import Invite
|
||||
from .template import Template
|
||||
from .widget import Widget
|
||||
@ -57,16 +55,14 @@ from .iterators import GuildIterator
|
||||
from .appinfo import AppInfo
|
||||
from .colour import Color, Colour
|
||||
|
||||
__all__ = (
|
||||
'Client',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def _cancel_tasks(loop):
|
||||
try:
|
||||
task_retriever = asyncio.Task.all_tasks
|
||||
except AttributeError:
|
||||
# future proofing for 3.9 I guess
|
||||
task_retriever = asyncio.all_tasks
|
||||
|
||||
tasks = {t for t in task_retriever(loop=loop) if not t.done()}
|
||||
tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()}
|
||||
|
||||
if not tasks:
|
||||
return
|
||||
@ -91,28 +87,11 @@ def _cancel_tasks(loop):
|
||||
def _cleanup_loop(loop):
|
||||
try:
|
||||
_cancel_tasks(loop)
|
||||
if sys.version_info >= (3, 6):
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
finally:
|
||||
log.info('Closing the event loop.')
|
||||
loop.close()
|
||||
|
||||
class _ClientEventTask(asyncio.Task):
|
||||
def __init__(self, original_coro, event_name, coro, *, loop):
|
||||
super().__init__(coro, loop=loop)
|
||||
self.__event_name = event_name
|
||||
self.__original_coro = original_coro
|
||||
|
||||
def __repr__(self):
|
||||
info = [
|
||||
('state', self._state.lower()),
|
||||
('event', self.__event_name),
|
||||
('coro', repr(self.__original_coro)),
|
||||
]
|
||||
if self._exception is not None:
|
||||
info.append(('exception', repr(self._exception)))
|
||||
return '<ClientEventTask {}>'.format(' '.join('%s=%s' % t for t in info))
|
||||
|
||||
class Client:
|
||||
r"""Represents a client connection that connects to Discord.
|
||||
This class is used to interact with the Discord WebSocket and API.
|
||||
@ -141,6 +120,8 @@ class Client:
|
||||
Integer starting at ``0`` and less than :attr:`.shard_count`.
|
||||
shard_count: Optional[:class:`int`]
|
||||
The total number of shards.
|
||||
application_id: :class:`int`
|
||||
The client's application ID.
|
||||
intents: :class:`Intents`
|
||||
The intents that you want to enable for the session. This is a way of
|
||||
disabling and enabling certain gateway events from triggering and being sent.
|
||||
@ -344,10 +325,7 @@ class Client:
|
||||
|
||||
def _get_state(self, **options):
|
||||
return ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
|
||||
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
|
||||
|
||||
async def _syncer(self, guilds):
|
||||
await self.ws.request_sync(guilds)
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
|
||||
def _handle_ready(self):
|
||||
self._ready.set()
|
||||
@ -415,6 +393,16 @@ class Client:
|
||||
"""
|
||||
return self._connection.voice_clients
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
"""Optional[:class:`int`]: The client's application ID.
|
||||
|
||||
If this is not passed via ``__init__`` then this is retrieved
|
||||
through the gateway when an event contains the data. Usually
|
||||
after :func:`on_connect` is called.
|
||||
"""
|
||||
return self._connection.application_id
|
||||
|
||||
def is_ready(self):
|
||||
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
|
||||
return self._ready.is_set()
|
||||
@ -433,7 +421,7 @@ class Client:
|
||||
def _schedule_event(self, coro, event_name, *args, **kwargs):
|
||||
wrapped = self._run_event(coro, event_name, *args, **kwargs)
|
||||
# Schedules the task
|
||||
return _ClientEventTask(original_coro=coro, event_name=event_name, coro=wrapped, loop=self.loop)
|
||||
return asyncio.create_task(wrapped, name=f'discord.py: {event_name}')
|
||||
|
||||
def dispatch(self, event, *args, **kwargs):
|
||||
log.debug('Dispatching event %s', event)
|
||||
@ -484,43 +472,9 @@ class Client:
|
||||
overridden to have a different implementation.
|
||||
Check :func:`~discord.on_error` for more details.
|
||||
"""
|
||||
print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
|
||||
print(f'Ignoring exception in {event_method}', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
@utils.deprecated('Guild.chunk')
|
||||
async def request_offline_members(self, *guilds):
|
||||
r"""|coro|
|
||||
|
||||
Requests previously offline members from the guild to be filled up
|
||||
into the :attr:`.Guild.members` cache. This function is usually not
|
||||
called. It should only be used if you have the ``fetch_offline_members``
|
||||
parameter set to ``False``.
|
||||
|
||||
When the client logs on and connects to the websocket, Discord does
|
||||
not provide the library with offline members if the number of members
|
||||
in the guild is larger than 250. You can check if a guild is large
|
||||
if :attr:`.Guild.large` is ``True``.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is deprecated. Use :meth:`Guild.chunk` instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*guilds: :class:`.Guild`
|
||||
An argument list of guilds to request offline members for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.InvalidArgument`
|
||||
If any guild is unavailable in the collection.
|
||||
"""
|
||||
if any(g.unavailable for g in guilds):
|
||||
raise InvalidArgument('An unavailable guild was passed.')
|
||||
|
||||
for guild in guilds:
|
||||
await self._connection.chunk_guild(guild)
|
||||
|
||||
# hooks
|
||||
|
||||
async def _call_before_identify_hook(self, shard_id, *, initial=False):
|
||||
@ -553,7 +507,7 @@ class Client:
|
||||
|
||||
# login state management
|
||||
|
||||
async def login(self, token, *, bot=True):
|
||||
async def login(self, token):
|
||||
"""|coro|
|
||||
|
||||
Logs in the client with the specified credentials.
|
||||
@ -572,9 +526,6 @@ class Client:
|
||||
token: :class:`str`
|
||||
The authentication token. Do not prefix this token with
|
||||
anything as the library will do it for you.
|
||||
bot: :class:`bool`
|
||||
Keyword argument that specifies if the account logging on is a bot
|
||||
token or not.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -587,21 +538,7 @@ class Client:
|
||||
"""
|
||||
|
||||
log.info('logging in using static token')
|
||||
await self.http.static_login(token.strip(), bot=bot)
|
||||
self._connection.is_bot = bot
|
||||
|
||||
async def logout(self):
|
||||
"""|coro|
|
||||
|
||||
Logs out of Discord and closes all connections.
|
||||
|
||||
.. note::
|
||||
|
||||
This is just an alias to :meth:`close`. If you want
|
||||
to do extraneous cleanup when subclassing, it is suggested
|
||||
to override :meth:`close` instead.
|
||||
"""
|
||||
await self.close()
|
||||
await self.http.static_login(token.strip())
|
||||
|
||||
async def connect(self, *, reconnect=True):
|
||||
"""|coro|
|
||||
@ -722,7 +659,7 @@ class Client:
|
||||
self._connection.clear()
|
||||
self.http.recreate()
|
||||
|
||||
async def start(self, *args, **kwargs):
|
||||
async def start(self, token, *, reconnect=True):
|
||||
"""|coro|
|
||||
|
||||
A shorthand coroutine for :meth:`login` + :meth:`connect`.
|
||||
@ -732,13 +669,7 @@ class Client:
|
||||
TypeError
|
||||
An unexpected keyword argument was received.
|
||||
"""
|
||||
bot = kwargs.pop('bot', True)
|
||||
reconnect = kwargs.pop('reconnect', True)
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("unexpected keyword argument(s) %s" % list(kwargs.keys()))
|
||||
|
||||
await self.login(*args, bot=bot)
|
||||
await self.login(token)
|
||||
await self.connect(reconnect=reconnect)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
@ -754,7 +685,7 @@ class Client:
|
||||
try:
|
||||
loop.run_until_complete(start(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
loop.run_until_complete(logout())
|
||||
loop.run_until_complete(close())
|
||||
# cancel all tasks lingering
|
||||
finally:
|
||||
loop.close()
|
||||
@ -833,12 +764,10 @@ class Client:
|
||||
|
||||
@allowed_mentions.setter
|
||||
def allowed_mentions(self, value):
|
||||
if value is None:
|
||||
self._connection.allowed_mentions = value
|
||||
elif isinstance(value, AllowedMentions):
|
||||
if value is None or isinstance(value, AllowedMentions):
|
||||
self._connection.allowed_mentions = value
|
||||
else:
|
||||
raise TypeError('allowed_mentions must be AllowedMentions not {0.__class__!r}'.format(value))
|
||||
raise TypeError(f'allowed_mentions must be AllowedMentions not {value.__class__!r}')
|
||||
|
||||
@property
|
||||
def intents(self):
|
||||
@ -937,8 +866,7 @@ class Client:
|
||||
"""
|
||||
|
||||
for guild in self.guilds:
|
||||
for channel in guild.channels:
|
||||
yield channel
|
||||
yield from guild.channels
|
||||
|
||||
def get_all_members(self):
|
||||
"""Returns a generator with every :class:`.Member` the client can see.
|
||||
@ -955,8 +883,7 @@ class Client:
|
||||
A member the client can see.
|
||||
"""
|
||||
for guild in self.guilds:
|
||||
for member in guild.members:
|
||||
yield member
|
||||
yield from guild.members
|
||||
|
||||
# listeners/waiters
|
||||
|
||||
@ -1154,9 +1081,7 @@ class Client:
|
||||
# Guild stuff
|
||||
|
||||
def fetch_guilds(self, *, limit=100, before=None, after=None):
|
||||
"""|coro|
|
||||
|
||||
Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
||||
"""Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -1191,10 +1116,12 @@ class Client:
|
||||
Defaults to ``100``.
|
||||
before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieves guilds before this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve guilds after this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -1306,15 +1233,13 @@ class Client:
|
||||
if icon is not None:
|
||||
icon = utils._bytes_to_base64_data(icon)
|
||||
|
||||
if region is None:
|
||||
region = VoiceRegion.us_west.value
|
||||
else:
|
||||
region = region.value
|
||||
region = region or VoiceRegion.us_west
|
||||
region_value = region.value
|
||||
|
||||
if code:
|
||||
data = await self.http.create_from_template(code, name, region, icon)
|
||||
data = await self.http.create_from_template(code, name, region_value, icon)
|
||||
else:
|
||||
data = await self.http.create_guild(name, region, icon)
|
||||
data = await self.http.create_guild(name, region_value, icon)
|
||||
return Guild(data=data, state=self._connection)
|
||||
|
||||
# Invite management
|
||||
@ -1502,48 +1427,6 @@ class Client:
|
||||
data = await self.http.get_user(user_id)
|
||||
return User(state=self._connection, data=data)
|
||||
|
||||
async def fetch_user_profile(self, user_id):
|
||||
"""|coro|
|
||||
|
||||
Gets an arbitrary user's profile.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
user_id: :class:`int`
|
||||
The ID of the user to fetch their profile for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.Forbidden`
|
||||
Not allowed to fetch profiles.
|
||||
:exc:`.HTTPException`
|
||||
Fetching the profile failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`.Profile`
|
||||
The profile of the user.
|
||||
"""
|
||||
|
||||
state = self._connection
|
||||
data = await self.http.get_user_profile(user_id)
|
||||
|
||||
def transform(d):
|
||||
return state._get_guild(int(d['id']))
|
||||
|
||||
since = data.get('premium_since')
|
||||
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
|
||||
user = data['user']
|
||||
return Profile(flags=user.get('flags', 0),
|
||||
premium_since=utils.parse_time(since),
|
||||
mutual_guilds=mutual_guilds,
|
||||
user=User(data=user, state=state),
|
||||
connected_accounts=data['connected_accounts'])
|
||||
|
||||
async def fetch_channel(self, channel_id):
|
||||
"""|coro|
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,6 +25,11 @@ DEALINGS IN THE SOFTWARE.
|
||||
import colorsys
|
||||
import random
|
||||
|
||||
__all__ = (
|
||||
'Colour',
|
||||
'Color',
|
||||
)
|
||||
|
||||
class Colour:
|
||||
"""Represents a Discord role colour. This class is similar
|
||||
to a (red, green, blue) :class:`tuple`.
|
||||
@ -61,7 +64,7 @@ class Colour:
|
||||
|
||||
def __init__(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int parameter, received %s instead.' % value.__class__.__name__)
|
||||
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
|
||||
|
||||
self.value = value
|
||||
|
||||
@ -75,10 +78,10 @@ class Colour:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return '#{:0>6x}'.format(self.value)
|
||||
return f'#{self.value:0>6x}'
|
||||
|
||||
def __repr__(self):
|
||||
return '<Colour value=%s>' % self.value
|
||||
return f'<Colour value={self.value}>'
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
@ -119,18 +122,32 @@ class Colour:
|
||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||
|
||||
@classmethod
|
||||
def random(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a random value.
|
||||
|
||||
.. versionadded:: 1.6"""
|
||||
return cls(random.randint(0x000000,0xffffff))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def random(cls, *, seed=None):
|
||||
"""A factory method that returns a :class:`Colour` with a random hue.
|
||||
|
||||
.. note::
|
||||
|
||||
The random algorithm works by choosing a colour with a random hue but
|
||||
with maxed out saturation and value.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
------------
|
||||
seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]]
|
||||
The seed to initialize the RNG with. If ``None`` is passed the default RNG is used.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
rand = random if seed is None else random.Random(seed)
|
||||
return cls.from_hsv(rand.random(), 1, 1)
|
||||
|
||||
@classmethod
|
||||
def teal(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import asyncio
|
||||
|
||||
__all__ = (
|
||||
'Typing',
|
||||
)
|
||||
|
||||
def _typing_done_callback(fut):
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,37 +22,88 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
|
||||
|
||||
from . import utils
|
||||
from .colour import Colour
|
||||
|
||||
__all__ = (
|
||||
'Embed',
|
||||
)
|
||||
|
||||
|
||||
class _EmptyEmbed:
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'Embed.Empty'
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return 0
|
||||
|
||||
EmptyEmbed = _EmptyEmbed()
|
||||
|
||||
EmptyEmbed: Final = _EmptyEmbed()
|
||||
|
||||
|
||||
class EmbedProxy:
|
||||
def __init__(self, layer):
|
||||
def __init__(self, layer: Dict[str, Any]):
|
||||
self.__dict__.update(layer)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
return 'EmbedProxy(%s)' % ', '.join(('%s=%r' % (k, v) for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||
def __repr__(self) -> str:
|
||||
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||
return f'EmbedProxy({inner})'
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> _EmptyEmbed:
|
||||
return EmptyEmbed
|
||||
|
||||
|
||||
E = TypeVar('E', bound='Embed')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from discord.types.embed import Embed as EmbedData, EmbedType
|
||||
|
||||
T = TypeVar('T')
|
||||
MaybeEmpty = Union[T, _EmptyEmbed]
|
||||
|
||||
class _EmbedFooterProxy(Protocol):
|
||||
text: MaybeEmpty[str]
|
||||
icon_url: MaybeEmpty[str]
|
||||
|
||||
class _EmbedFieldProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
value: MaybeEmpty[str]
|
||||
inline: bool
|
||||
|
||||
class _EmbedMediaProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
proxy_url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
class _EmbedVideoProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
class _EmbedProviderProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
|
||||
class _EmbedAuthorProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
icon_url: MaybeEmpty[str]
|
||||
proxy_icon_url: MaybeEmpty[str]
|
||||
|
||||
|
||||
class Embed:
|
||||
"""Represents a Discord embed.
|
||||
|
||||
@ -65,6 +114,12 @@ class Embed:
|
||||
Returns the total size of the embed.
|
||||
Useful for checking if it's within the 6000 character limit.
|
||||
|
||||
.. describe:: bool(b)
|
||||
|
||||
Returns whether the embed has any data set.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Certain properties return an ``EmbedProxy``, a type
|
||||
that acts similar to a regular :class:`dict` except using dotted access,
|
||||
e.g. ``embed.author.icon_url``. If the attribute
|
||||
@ -82,6 +137,8 @@ class Embed:
|
||||
type: :class:`str`
|
||||
The type of embed. Usually "rich".
|
||||
This can be set during initialisation.
|
||||
Possible strings for embed types can be found on discord's
|
||||
`api docs <https://discord.com/developers/docs/resources/channel#embed-object-embed-types>`_
|
||||
description: :class:`str`
|
||||
The description of the embed.
|
||||
This can be set during initialisation.
|
||||
@ -89,7 +146,9 @@ class Embed:
|
||||
The URL of the embed.
|
||||
This can be set during initialisation.
|
||||
timestamp: :class:`datetime.datetime`
|
||||
The timestamp of the embed content. This could be a naive or aware datetime.
|
||||
The timestamp of the embed content. This is an aware datetime.
|
||||
If a naive datetime is passed, it is converted to an aware
|
||||
datetime with the local timezone.
|
||||
colour: Union[:class:`Colour`, :class:`int`]
|
||||
The colour code of the embed. Aliased to ``color`` as well.
|
||||
This can be set during initialisation.
|
||||
@ -98,37 +157,64 @@ class Embed:
|
||||
to denote that the value or attribute is empty.
|
||||
"""
|
||||
|
||||
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
|
||||
'_image', '_thumbnail', '_video', '_provider', '_author',
|
||||
'_fields', 'description')
|
||||
__slots__ = (
|
||||
'title',
|
||||
'url',
|
||||
'type',
|
||||
'_timestamp',
|
||||
'_colour',
|
||||
'_footer',
|
||||
'_image',
|
||||
'_thumbnail',
|
||||
'_video',
|
||||
'_provider',
|
||||
'_author',
|
||||
'_fields',
|
||||
'description',
|
||||
)
|
||||
|
||||
Empty = EmptyEmbed
|
||||
Empty: Final = EmptyEmbed
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# swap the colour/color aliases
|
||||
try:
|
||||
colour = kwargs['colour']
|
||||
except KeyError:
|
||||
colour = kwargs.get('color', os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed))
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
title: MaybeEmpty[str] = EmptyEmbed,
|
||||
type: EmbedType = 'rich',
|
||||
url: MaybeEmpty[str] = EmptyEmbed,
|
||||
description: MaybeEmpty[str] = EmptyEmbed,
|
||||
timestamp: datetime.datetime = None,
|
||||
):
|
||||
|
||||
if colour is EmptyEmbed and color is EmptyEmbed:
|
||||
colour = os.getenv("DEFAULT_EMBED_COLOR", default=EmptyEmbed)
|
||||
if isinstance(colour, str):
|
||||
colour = int(colour, 16)
|
||||
|
||||
|
||||
self.colour = colour
|
||||
self.title = kwargs.get('title', EmptyEmbed)
|
||||
self.type = kwargs.get('type', 'rich')
|
||||
self.url = kwargs.get('url', EmptyEmbed)
|
||||
self.description = kwargs.get('description', EmptyEmbed)
|
||||
|
||||
try:
|
||||
timestamp = kwargs['timestamp']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
colour = colour if colour is not EmptyEmbed else color
|
||||
self.colour = colour
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.description = description
|
||||
|
||||
if self.title is not EmptyEmbed:
|
||||
self.title = str(self.title)
|
||||
|
||||
if self.description is not EmptyEmbed:
|
||||
self.description = str(self.description)
|
||||
|
||||
if self.url is not EmptyEmbed:
|
||||
self.url = str(self.url)
|
||||
|
||||
if timestamp:
|
||||
if timestamp.tzinfo is None:
|
||||
timestamp = timestamp.astimezone()
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls: Type[E], data: EmbedData) -> E:
|
||||
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
|
||||
format that Discord expects it to be in.
|
||||
|
||||
@ -144,7 +230,7 @@ class Embed:
|
||||
The dictionary to convert into an embed.
|
||||
"""
|
||||
# we are bypassing __init__ here since it doesn't apply here
|
||||
self = cls.__new__(cls)
|
||||
self: E = cls.__new__(cls)
|
||||
|
||||
# fill in the basic fields
|
||||
|
||||
@ -153,6 +239,15 @@ class Embed:
|
||||
self.description = data.get('description', EmptyEmbed)
|
||||
self.url = data.get('url', EmptyEmbed)
|
||||
|
||||
if self.title is not EmptyEmbed:
|
||||
self.title = str(self.title)
|
||||
|
||||
if self.description is not EmptyEmbed:
|
||||
self.description = str(self.description)
|
||||
|
||||
if self.url is not EmptyEmbed:
|
||||
self.url = str(self.url)
|
||||
|
||||
# try to fill in the more rich fields
|
||||
|
||||
try:
|
||||
@ -175,11 +270,11 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
def copy(self: E) -> E:
|
||||
"""Returns a shallow copy of the embed."""
|
||||
return Embed.from_dict(self.to_dict())
|
||||
return self.__class__.from_dict(self.to_dict())
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
total = len(self.title) + len(self.description)
|
||||
for field in getattr(self, '_fields', []):
|
||||
total += len(field['name']) + len(field['value'])
|
||||
@ -200,43 +295,61 @@ class Embed:
|
||||
|
||||
return total
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return any(
|
||||
(
|
||||
self.title,
|
||||
self.url,
|
||||
self.description,
|
||||
self.colour,
|
||||
self.fields,
|
||||
self.timestamp,
|
||||
self.author,
|
||||
self.thumbnail,
|
||||
self.footer,
|
||||
self.image,
|
||||
self.provider,
|
||||
self.video,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
def colour(self) -> MaybeEmpty[Colour]:
|
||||
return getattr(self, '_colour', EmptyEmbed)
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value):
|
||||
def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
|
||||
if isinstance(value, (Colour, _EmptyEmbed)):
|
||||
self._colour = value
|
||||
elif isinstance(value, int):
|
||||
self._colour = Colour(value=value)
|
||||
else:
|
||||
raise TypeError('Expected discord.Colour, int, or Embed.Empty but received %s instead.' % value.__class__.__name__)
|
||||
raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.')
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
def timestamp(self) -> MaybeEmpty[datetime.datetime]:
|
||||
return getattr(self, '_timestamp', EmptyEmbed)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, value):
|
||||
def timestamp(self, value: MaybeEmpty[datetime.datetime]):
|
||||
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
|
||||
self._timestamp = value
|
||||
else:
|
||||
raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__)
|
||||
raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
|
||||
|
||||
@property
|
||||
def footer(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the footer contents.
|
||||
def footer(self) -> _EmbedFooterProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the footer contents.
|
||||
|
||||
See :meth:`set_footer` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_footer', {}))
|
||||
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
|
||||
|
||||
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
"""Sets the footer for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -260,8 +373,8 @@ class Embed:
|
||||
return self
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the image contents.
|
||||
def image(self) -> _EmbedMediaProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
@ -272,9 +385,9 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_image', {}))
|
||||
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
||||
|
||||
def set_image(self, *, url):
|
||||
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -296,14 +409,14 @@ class Embed:
|
||||
pass
|
||||
else:
|
||||
self._image = {
|
||||
'url': str(url)
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
def thumbnail(self) -> _EmbedMediaProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
@ -314,9 +427,9 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {}))
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||
|
||||
def set_thumbnail(self, *, url):
|
||||
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -338,14 +451,14 @@ class Embed:
|
||||
pass
|
||||
else:
|
||||
self._thumbnail = {
|
||||
'url': str(url)
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the video contents.
|
||||
def video(self) -> _EmbedVideoProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the video contents.
|
||||
|
||||
Possible attributes include:
|
||||
|
||||
@ -355,29 +468,29 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_video', {}))
|
||||
return EmbedProxy(getattr(self, '_video', {})) # type: ignore
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the provider contents.
|
||||
def provider(self) -> _EmbedProviderProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the provider contents.
|
||||
|
||||
The only attributes that might be accessed are ``name`` and ``url``.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_provider', {}))
|
||||
return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the author contents.
|
||||
def author(self) -> _EmbedAuthorProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the author contents.
|
||||
|
||||
See :meth:`set_author` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_author', {}))
|
||||
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
|
||||
|
||||
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
"""Sets the author for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -394,7 +507,7 @@ class Embed:
|
||||
"""
|
||||
|
||||
self._author = {
|
||||
'name': str(name)
|
||||
'name': str(name),
|
||||
}
|
||||
|
||||
if url is not EmptyEmbed:
|
||||
@ -405,7 +518,7 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def remove_author(self):
|
||||
def remove_author(self: E) -> E:
|
||||
"""Clears embed's author information.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -421,16 +534,16 @@ class Embed:
|
||||
return self
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
def fields(self) -> List[_EmbedFieldProxy]:
|
||||
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
||||
|
||||
See :meth:`add_field` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])]
|
||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
|
||||
|
||||
def add_field(self, *, name, value, inline=True):
|
||||
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Adds a field to the embed object.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -449,7 +562,7 @@ class Embed:
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
'value': str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@ -459,7 +572,7 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def insert_field_at(self, index, *, name, value, inline=True):
|
||||
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Inserts a field before a specified index to the embed.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@ -482,7 +595,7 @@ class Embed:
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value)
|
||||
'value': str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@ -492,14 +605,14 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def clear_fields(self):
|
||||
def clear_fields(self) -> None:
|
||||
"""Removes all fields from this embed."""
|
||||
try:
|
||||
self._fields.clear()
|
||||
except AttributeError:
|
||||
self._fields = []
|
||||
|
||||
def remove_field(self, index):
|
||||
def remove_field(self, index: int) -> None:
|
||||
"""Removes a field at a specified index.
|
||||
|
||||
If the index is invalid or out of bounds then the error is
|
||||
@ -520,7 +633,7 @@ class Embed:
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
def set_field_at(self, index, *, name, value, inline=True):
|
||||
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
"""Modifies a field to the embed object.
|
||||
|
||||
The index must point to a valid pre-existing field.
|
||||
@ -555,15 +668,17 @@ class Embed:
|
||||
field['inline'] = inline
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> EmbedData:
|
||||
"""Converts this embed object into a dict."""
|
||||
|
||||
# add in the raw data into the dict
|
||||
# fmt: off
|
||||
result = {
|
||||
key[1:]: getattr(self, key)
|
||||
for key in self.__slots__
|
||||
if key[0] == '_' and hasattr(self, key)
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
# deal with basic convenience wrappers
|
||||
|
||||
@ -599,4 +714,4 @@ class Embed:
|
||||
if self.title:
|
||||
result['title'] = self.title
|
||||
|
||||
return result
|
||||
return result # type: ignore
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,10 @@ from . import utils
|
||||
from .partial_emoji import _EmojiTag
|
||||
from .user import User
|
||||
|
||||
__all__ = (
|
||||
'Emoji',
|
||||
)
|
||||
|
||||
class Emoji(_EmojiTag):
|
||||
"""Represents a custom emoji.
|
||||
|
||||
|
120
discord/enums.py
120
discord/enums.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -26,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import types
|
||||
from collections import namedtuple
|
||||
from typing import Any, TYPE_CHECKING, Type, TypeVar
|
||||
|
||||
__all__ = (
|
||||
'Enum',
|
||||
@ -37,18 +36,12 @@ __all__ = (
|
||||
'ContentFilter',
|
||||
'Status',
|
||||
'DefaultAvatar',
|
||||
'RelationshipType',
|
||||
'AuditLogAction',
|
||||
'AuditLogActionCategory',
|
||||
'UserFlags',
|
||||
'ActivityType',
|
||||
'HypeSquadHouse',
|
||||
'NotificationLevel',
|
||||
'PremiumType',
|
||||
'UserContentFilter',
|
||||
'FriendFlags',
|
||||
'TeamMembershipState',
|
||||
'Theme',
|
||||
'WebhookType',
|
||||
'ExpireBehaviour',
|
||||
'ExpireBehavior',
|
||||
@ -57,8 +50,8 @@ __all__ = (
|
||||
|
||||
def _create_value_cls(name):
|
||||
cls = namedtuple('_EnumValue_' + name, 'name value')
|
||||
cls.__repr__ = lambda self: '<%s.%s: %r>' % (name, self.name, self.value)
|
||||
cls.__str__ = lambda self: '%s.%s' % (name, self.name)
|
||||
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
|
||||
cls.__str__ = lambda self: f'{name}.{self.name}'
|
||||
return cls
|
||||
|
||||
def _is_descriptor(obj):
|
||||
@ -98,6 +91,7 @@ class EnumMeta(type):
|
||||
attrs['_enum_value_map_'] = value_mapping
|
||||
attrs['_enum_member_map_'] = member_mapping
|
||||
attrs['_enum_member_names_'] = member_names
|
||||
attrs['_enum_value_cls_'] = value_cls
|
||||
actual_cls = super().__new__(cls, name, bases, attrs)
|
||||
value_cls._actual_enum_cls_ = actual_cls
|
||||
return actual_cls
|
||||
@ -112,7 +106,7 @@ class EnumMeta(type):
|
||||
return len(cls._enum_member_names_)
|
||||
|
||||
def __repr__(cls):
|
||||
return '<enum %r>' % cls.__name__
|
||||
return f'<enum {cls.__name__}>'
|
||||
|
||||
@property
|
||||
def __members__(cls):
|
||||
@ -122,7 +116,7 @@ class EnumMeta(type):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
|
||||
raise ValueError(f"{value!r} is not a valid {cls.__name__}")
|
||||
|
||||
def __getitem__(cls, key):
|
||||
return cls._enum_member_map_[key]
|
||||
@ -141,14 +135,16 @@ class EnumMeta(type):
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
class Enum(metaclass=EnumMeta):
|
||||
@classmethod
|
||||
def try_value(cls, value):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
return value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enum import Enum
|
||||
else:
|
||||
class Enum(metaclass=EnumMeta):
|
||||
@classmethod
|
||||
def try_value(cls, value):
|
||||
try:
|
||||
return cls._enum_value_map_[value]
|
||||
except (KeyError, TypeError):
|
||||
return value
|
||||
|
||||
class ChannelType(Enum):
|
||||
text = 0
|
||||
@ -158,24 +154,30 @@ class ChannelType(Enum):
|
||||
category = 4
|
||||
news = 5
|
||||
store = 6
|
||||
stage_voice = 13
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class MessageType(Enum):
|
||||
default = 0
|
||||
recipient_add = 1
|
||||
recipient_remove = 2
|
||||
call = 3
|
||||
channel_name_change = 4
|
||||
channel_icon_change = 5
|
||||
pins_add = 6
|
||||
new_member = 7
|
||||
premium_guild_subscription = 8
|
||||
premium_guild_tier_1 = 9
|
||||
premium_guild_tier_2 = 10
|
||||
premium_guild_tier_3 = 11
|
||||
channel_follow_add = 12
|
||||
default = 0
|
||||
recipient_add = 1
|
||||
recipient_remove = 2
|
||||
call = 3
|
||||
channel_name_change = 4
|
||||
channel_icon_change = 5
|
||||
pins_add = 6
|
||||
new_member = 7
|
||||
premium_guild_subscription = 8
|
||||
premium_guild_tier_1 = 9
|
||||
premium_guild_tier_2 = 10
|
||||
premium_guild_tier_3 = 11
|
||||
channel_follow_add = 12
|
||||
guild_stream = 13
|
||||
guild_discovery_disqualified = 14
|
||||
guild_discovery_requalified = 15
|
||||
guild_discovery_grace_period_initial_warning = 16
|
||||
guild_discovery_grace_period_final_warning = 17
|
||||
|
||||
class VoiceRegion(Enum):
|
||||
us_west = 'us-west'
|
||||
@ -238,22 +240,6 @@ class ContentFilter(Enum):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class UserContentFilter(Enum):
|
||||
disabled = 0
|
||||
friends = 1
|
||||
all_messages = 2
|
||||
|
||||
class FriendFlags(Enum):
|
||||
noone = 0
|
||||
mutual_guilds = 1
|
||||
mutual_friends = 2
|
||||
guild_and_friends = 3
|
||||
everyone = 4
|
||||
|
||||
class Theme(Enum):
|
||||
light = 'light'
|
||||
dark = 'dark'
|
||||
|
||||
class Status(Enum):
|
||||
online = 'online'
|
||||
offline = 'offline'
|
||||
@ -276,12 +262,6 @@ class DefaultAvatar(Enum):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class RelationshipType(Enum):
|
||||
friend = 1
|
||||
blocked = 2
|
||||
incoming_request = 3
|
||||
outgoing_request = 4
|
||||
|
||||
class NotificationLevel(Enum):
|
||||
all_messages = 0
|
||||
only_mentions = 1
|
||||
@ -423,15 +403,6 @@ class ActivityType(Enum):
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
class HypeSquadHouse(Enum):
|
||||
bravery = 1
|
||||
brilliance = 2
|
||||
balance = 3
|
||||
|
||||
class PremiumType(Enum):
|
||||
nitro_classic = 1
|
||||
nitro = 2
|
||||
|
||||
class TeamMembershipState(Enum):
|
||||
invited = 1
|
||||
accepted = 2
|
||||
@ -451,13 +422,24 @@ class StickerType(Enum):
|
||||
apng = 2
|
||||
lottie = 3
|
||||
|
||||
def try_enum(cls, val):
|
||||
class InteractionType(Enum):
|
||||
ping = 1
|
||||
application_command = 2
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def create_unknown_value(cls: Type[T], val: Any) -> T:
|
||||
value_cls = cls._enum_value_cls_ # type: ignore
|
||||
name = f'unknown_{val}'
|
||||
return value_cls(name=name, value=val)
|
||||
|
||||
def try_enum(cls: Type[T], val: Any) -> T:
|
||||
"""A function that tries to turn the value into enum ``cls``.
|
||||
|
||||
If it fails it returns the value instead.
|
||||
If it fails it returns a proxy invalid value instead.
|
||||
"""
|
||||
|
||||
try:
|
||||
return cls._enum_value_map_[val]
|
||||
return cls._enum_value_map_[val] # type: ignore
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
return val
|
||||
return create_unknown_value(cls, val)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,6 +22,22 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'DiscordException',
|
||||
'ClientException',
|
||||
'NoMoreItems',
|
||||
'GatewayNotFound',
|
||||
'HTTPException',
|
||||
'Forbidden',
|
||||
'NotFound',
|
||||
'DiscordServerError',
|
||||
'InvalidData',
|
||||
'InvalidArgument',
|
||||
'LoginFailure',
|
||||
'ConnectionClosed',
|
||||
'PrivilegedIntentsRequired',
|
||||
)
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for discord.py
|
||||
|
||||
@ -48,7 +62,7 @@ class GatewayNotFound(DiscordException):
|
||||
for the :class:`Client` websocket is not found."""
|
||||
def __init__(self):
|
||||
message = 'The gateway to connect to discord was not found.'
|
||||
super(GatewayNotFound, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
def flatten_error_dict(d, key=''):
|
||||
items = []
|
||||
@ -104,7 +118,7 @@ class HTTPException(DiscordException):
|
||||
|
||||
fmt = '{0.status} {0.reason} (error code: {1})'
|
||||
if len(self.text):
|
||||
fmt = fmt + ': {2}'
|
||||
fmt += ': {2}'
|
||||
|
||||
super().__init__(fmt.format(self.response, self.code, self.text))
|
||||
|
||||
@ -174,7 +188,7 @@ class ConnectionClosed(ClientException):
|
||||
# aiohttp doesn't seem to consistently provide close reason
|
||||
self.reason = ''
|
||||
self.shard_id = shard_id
|
||||
super().__init__('Shard ID %s WebSocket closed with %s' % (self.shard_id, self.code))
|
||||
super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}')
|
||||
|
||||
class PrivilegedIntentsRequired(ClientException):
|
||||
"""Exception that's thrown when the gateway is requesting privileged intents
|
||||
|
@ -1,17 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
discord.ext.commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2015-2020 Rapptz
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
|
||||
from .context import Context
|
||||
from .bot import *
|
||||
from .context import *
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .help import *
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -42,12 +40,19 @@ from . import errors
|
||||
from .help import HelpCommand, DefaultHelpCommand
|
||||
from .cog import Cog
|
||||
|
||||
__all__ = (
|
||||
'when_mentioned',
|
||||
'when_mentioned_or',
|
||||
'Bot',
|
||||
'AutoShardedBot',
|
||||
)
|
||||
|
||||
def when_mentioned(bot, msg):
|
||||
"""A callable that implements a command prefix equivalent to being mentioned.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
"""
|
||||
return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id]
|
||||
return [f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
|
||||
|
||||
def when_mentioned_or(*prefixes):
|
||||
"""A callable that implements when mentioned or other prefixes provided.
|
||||
@ -110,12 +115,13 @@ class BotBase(GroupMixin):
|
||||
self.description = inspect.cleandoc(description) if description else ''
|
||||
self.owner_id = options.get('owner_id')
|
||||
self.owner_ids = options.get('owner_ids', set())
|
||||
self.strip_after_prefix = options.get('strip_after_prefix', False)
|
||||
|
||||
if self.owner_id and self.owner_ids:
|
||||
raise TypeError('Both owner_id and owner_ids are set.')
|
||||
|
||||
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
||||
raise TypeError('owner_ids must be a collection not {0.__class__!r}'.format(self.owner_ids))
|
||||
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
||||
|
||||
if options.pop('self_bot', False):
|
||||
self._skip_check = lambda x, y: x != y
|
||||
@ -190,11 +196,10 @@ class BotBase(GroupMixin):
|
||||
return
|
||||
|
||||
cog = context.cog
|
||||
if cog:
|
||||
if Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
|
||||
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr)
|
||||
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
# global check registration
|
||||
@ -656,7 +661,13 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.__extensions[key] = lib
|
||||
|
||||
def load_extension(self, name):
|
||||
def _resolve_name(self, name, package):
|
||||
try:
|
||||
return importlib.util.resolve_name(name, package)
|
||||
except ImportError:
|
||||
raise errors.ExtensionNotFound(name)
|
||||
|
||||
def load_extension(self, name, *, package=None):
|
||||
"""Loads an extension.
|
||||
|
||||
An extension is a python module that contains commands, cogs, or
|
||||
@ -672,11 +683,19 @@ class BotBase(GroupMixin):
|
||||
The extension name to load. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when loading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
--------
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionAlreadyLoaded
|
||||
The extension is already loaded.
|
||||
NoEntryPointError
|
||||
@ -685,6 +704,7 @@ class BotBase(GroupMixin):
|
||||
The extension or its setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
if name in self.__extensions:
|
||||
raise errors.ExtensionAlreadyLoaded(name)
|
||||
|
||||
@ -694,7 +714,7 @@ class BotBase(GroupMixin):
|
||||
|
||||
self._load_from_module_spec(spec, name)
|
||||
|
||||
def unload_extension(self, name):
|
||||
def unload_extension(self, name, *, package=None):
|
||||
"""Unloads an extension.
|
||||
|
||||
When the extension is unloaded, all commands, listeners, and cogs are
|
||||
@ -711,13 +731,23 @@ class BotBase(GroupMixin):
|
||||
The extension name to unload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when unloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
ExtensionNotFound
|
||||
The name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionNotLoaded
|
||||
The extension was not loaded.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@ -725,7 +755,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
|
||||
def reload_extension(self, name):
|
||||
def reload_extension(self, name, *, package=None):
|
||||
"""Atomically reloads an extension.
|
||||
|
||||
This replaces the extension with the same extension, only refreshed. This is
|
||||
@ -739,6 +769,12 @@ class BotBase(GroupMixin):
|
||||
The extension name to reload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when reloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -746,12 +782,15 @@ class BotBase(GroupMixin):
|
||||
The extension was not loaded.
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
NoEntryPointError
|
||||
The extension does not have a setup function.
|
||||
ExtensionFailed
|
||||
The extension setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@ -768,7 +807,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
self.load_extension(name)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# if the load failed, the remnants should have been
|
||||
# cleaned from the load_extension function call
|
||||
# so let's load it from our old compiled library.
|
||||
@ -920,6 +959,9 @@ class BotBase(GroupMixin):
|
||||
# Getting here shouldn't happen
|
||||
raise
|
||||
|
||||
if self.strip_after_prefix:
|
||||
view.skip_ws()
|
||||
|
||||
invoker = view.get_word()
|
||||
ctx.invoked_with = invoker
|
||||
ctx.prefix = invoked_prefix
|
||||
@ -949,7 +991,7 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.dispatch('command_completion', ctx)
|
||||
elif ctx.invoked_with:
|
||||
exc = errors.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with))
|
||||
exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found')
|
||||
self.dispatch('command_error', ctx, exc)
|
||||
|
||||
async def process_commands(self, message):
|
||||
@ -1054,6 +1096,12 @@ class Bot(BotBase, discord.Client):
|
||||
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
strip_after_prefix: :class:`bool`
|
||||
Whether to strip whitespace characters after encountering the command
|
||||
prefix. This allows for ``! hello`` and ``!hello`` to both work if
|
||||
the ``command_prefix`` is set to ``!``. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -101,7 +99,7 @@ class CogMeta(type):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, bases, attrs = args
|
||||
attrs['__cog_name__'] = kwargs.pop('name', name)
|
||||
attrs['__cog_settings__'] = command_attrs = kwargs.pop('command_attrs', {})
|
||||
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
|
||||
|
||||
aliases = kwargs.pop('aliases', [])
|
||||
if not isinstance(aliases, list):
|
||||
@ -130,13 +128,13 @@ class CogMeta(type):
|
||||
value = value.__func__
|
||||
if isinstance(value, _BaseCommand):
|
||||
if is_static_method:
|
||||
raise TypeError('Command in method {0}.{1!r} must not be staticmethod.'.format(base, elem))
|
||||
raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
commands[elem] = value
|
||||
elif inspect.iscoroutinefunction(value):
|
||||
try:
|
||||
is_listener = getattr(value, '__cog_listener__')
|
||||
getattr(value, '__cog_listener__')
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
@ -202,7 +200,7 @@ class Cog(metaclass=CogMeta):
|
||||
parent = lookup[parent.qualified_name]
|
||||
|
||||
# Update our parent's reference to our self
|
||||
removed = parent.remove_command(command.name)
|
||||
parent.remove_command(command.name)
|
||||
parent.add_command(command)
|
||||
|
||||
return self
|
||||
@ -285,7 +283,7 @@ class Cog(metaclass=CogMeta):
|
||||
"""
|
||||
|
||||
if name is not None and not isinstance(name, str):
|
||||
raise TypeError('Cog.listener expected str but received {0.__class__.__name__!r} instead.'.format(name))
|
||||
raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
|
||||
|
||||
def decorator(func):
|
||||
actual = func
|
||||
@ -306,6 +304,13 @@ class Cog(metaclass=CogMeta):
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the cog has an error handler.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
|
||||
|
||||
@_cog_special_method
|
||||
def cog_unload(self):
|
||||
"""A special method that is called when the cog gets removed.
|
||||
@ -411,7 +416,8 @@ class Cog(metaclass=CogMeta):
|
||||
except Exception as e:
|
||||
# undo our additions
|
||||
for to_undo in self.__cog_commands__[:index]:
|
||||
bot.remove_command(to_undo.name)
|
||||
if to_undo.parent is None:
|
||||
bot.remove_command(to_undo.name)
|
||||
raise e
|
||||
|
||||
# check if we're overriding the default
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -28,6 +26,10 @@ import re
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
|
||||
__all__ = (
|
||||
'Context',
|
||||
)
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
r"""Represents the context in which a command is being invoked under.
|
||||
|
||||
@ -58,6 +60,14 @@ class Context(discord.abc.Messageable):
|
||||
invoked_with: :class:`str`
|
||||
The command name that triggered this invocation. Useful for finding out
|
||||
which alias called the command.
|
||||
invoked_parents: List[:class:`str`]
|
||||
The command names of the parents that triggered this invocation. Useful for
|
||||
finding out which aliases called the command.
|
||||
|
||||
For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
invoked_subcommand: :class:`Command`
|
||||
The subcommand that was invoked.
|
||||
If no valid subcommand was invoked then this is equal to ``None``.
|
||||
@ -80,6 +90,7 @@ class Context(discord.abc.Messageable):
|
||||
self.command = attrs.pop('command', None)
|
||||
self.view = attrs.pop('view', None)
|
||||
self.invoked_with = attrs.pop('invoked_with', None)
|
||||
self.invoked_parents = attrs.pop('invoked_parents', [])
|
||||
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
||||
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
||||
self.command_failed = attrs.pop('command_failed', False)
|
||||
@ -94,7 +105,7 @@ class Context(discord.abc.Messageable):
|
||||
pattern = re.compile(r"<@!?%s>" % user.id)
|
||||
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
|
||||
|
||||
async def invoke(self, *args, **kwargs):
|
||||
async def invoke(self, command, /, *args, **kwargs):
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
@ -111,10 +122,6 @@ class Context(discord.abc.Messageable):
|
||||
You must take care in passing the proper arguments when
|
||||
using this function.
|
||||
|
||||
.. warning::
|
||||
|
||||
The first parameter passed **must** be the command being invoked.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`.Command`
|
||||
@ -129,18 +136,12 @@ class Context(discord.abc.Messageable):
|
||||
TypeError
|
||||
The command argument to invoke is missing.
|
||||
"""
|
||||
|
||||
try:
|
||||
command = args[0]
|
||||
except IndexError:
|
||||
raise TypeError('Missing command to invoke.') from None
|
||||
|
||||
arguments = []
|
||||
if command.cog is not None:
|
||||
arguments.append(command.cog)
|
||||
|
||||
arguments.append(self)
|
||||
arguments.extend(args[1:])
|
||||
arguments.extend(args)
|
||||
|
||||
ret = await command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
@ -184,13 +185,15 @@ class Context(discord.abc.Messageable):
|
||||
index, previous = view.index, view.previous
|
||||
invoked_with = self.invoked_with
|
||||
invoked_subcommand = self.invoked_subcommand
|
||||
invoked_parents = self.invoked_parents
|
||||
subcommand_passed = self.subcommand_passed
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.previous = 0
|
||||
view.get_word() # advance to get the root command
|
||||
self.invoked_parents = []
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
@ -202,6 +205,7 @@ class Context(discord.abc.Messageable):
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.invoked_parents = invoked_parents
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
@ -214,7 +218,7 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
""":class:`.Cog`: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
return None
|
||||
@ -227,8 +231,8 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
""":class:`.TextChannel`:
|
||||
Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`.
|
||||
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Message.channel`.
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@ -311,7 +315,7 @@ class Context(discord.abc.Messageable):
|
||||
entity = bot.get_cog(entity) or bot.get_command(entity)
|
||||
|
||||
try:
|
||||
qualified_name = entity.qualified_name
|
||||
entity.qualified_name
|
||||
except AttributeError:
|
||||
# if we're here then it's not a cog, group, or command.
|
||||
return None
|
||||
@ -333,7 +337,6 @@ class Context(discord.abc.Messageable):
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
|
||||
@discord.utils.copy_doc(discord.Message.reply)
|
||||
async def reply(self, content=None, **kwargs):
|
||||
return await self.message.reply(content, **kwargs)
|
||||
|
||||
reply.__doc__ = discord.Message.reply.__doc__
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,34 +22,44 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import inspect
|
||||
import typing
|
||||
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
|
||||
|
||||
import discord
|
||||
|
||||
from .errors import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Converter',
|
||||
'MemberConverter',
|
||||
'UserConverter',
|
||||
'MessageConverter',
|
||||
'PartialMessageConverter',
|
||||
'TextChannelConverter',
|
||||
'InviteConverter',
|
||||
'GuildConverter',
|
||||
'RoleConverter',
|
||||
'GameConverter',
|
||||
'ColourConverter',
|
||||
'ColorConverter',
|
||||
'VoiceChannelConverter',
|
||||
'StageChannelConverter',
|
||||
'EmojiConverter',
|
||||
'PartialEmojiConverter',
|
||||
'CategoryChannelConverter',
|
||||
'IDConverter',
|
||||
'StoreChannelConverter',
|
||||
'clean_content',
|
||||
'Greedy',
|
||||
)
|
||||
|
||||
|
||||
def _get_from_guilds(bot, getter, argument):
|
||||
result = None
|
||||
for guild in bot.guilds:
|
||||
@ -60,9 +68,13 @@ def _get_from_guilds(bot, getter, argument):
|
||||
return result
|
||||
return result
|
||||
|
||||
_utils_get = discord.utils.get
|
||||
|
||||
class Converter:
|
||||
_utils_get = discord.utils.get
|
||||
T = TypeVar('T', covariant=True)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Converter(Protocol[T]):
|
||||
"""The base class of custom converters that require the :class:`.Context`
|
||||
to be passed to be useful.
|
||||
|
||||
@ -73,7 +85,7 @@ class Converter:
|
||||
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> T:
|
||||
"""|coro|
|
||||
|
||||
The method to override to do conversion logic.
|
||||
@ -98,15 +110,15 @@ class Converter:
|
||||
"""
|
||||
raise NotImplementedError('Derived classes need to implement this.')
|
||||
|
||||
class IDConverter(Converter):
|
||||
class IDConverter(Converter[T]):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r'([0-9]{15,21})$')
|
||||
self._id_regex = re.compile(r'([0-9]{15,20})$')
|
||||
super().__init__()
|
||||
|
||||
def _get_id_match(self, argument):
|
||||
return self._id_regex.match(argument)
|
||||
|
||||
class MemberConverter(IDConverter):
|
||||
class MemberConverter(IDConverter[discord.Member]):
|
||||
"""Converts to a :class:`~discord.Member`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -159,7 +171,7 @@ class MemberConverter(IDConverter):
|
||||
return None
|
||||
return members[0]
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Member:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
guild = ctx.guild
|
||||
@ -192,7 +204,7 @@ class MemberConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class UserConverter(IDConverter):
|
||||
class UserConverter(IDConverter[discord.User]):
|
||||
"""Converts to a :class:`~discord.User`.
|
||||
|
||||
All lookups are via the global user cache.
|
||||
@ -211,7 +223,7 @@ class UserConverter(IDConverter):
|
||||
This converter now lazily fetches users from the HTTP APIs if an ID is passed
|
||||
and it's not available in cache.
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.User:
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
result = None
|
||||
state = ctx._state
|
||||
@ -251,7 +263,39 @@ class UserConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class MessageConverter(Converter):
|
||||
class PartialMessageConverter(Converter[discord.PartialMessage]):
|
||||
"""Converts to a :class:`discord.PartialMessage`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
The creation strategy is as follows (in order):
|
||||
|
||||
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
|
||||
2. By message ID (The message is assumed to be in the context channel.)
|
||||
3. By message URL
|
||||
"""
|
||||
@staticmethod
|
||||
def _get_id_matches(argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,20}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,20})/(?P<message_id>[0-9]{15,20})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
channel_id = match.group("channel_id")
|
||||
return int(match.group("message_id")), int(channel_id) if channel_id else None
|
||||
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
|
||||
message_id, channel_id = self._get_id_matches(argument)
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
return discord.PartialMessage(channel=channel, id=message_id)
|
||||
|
||||
class MessageConverter(IDConverter[discord.Message]):
|
||||
"""Converts to a :class:`discord.Message`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
@ -265,22 +309,12 @@ class MessageConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,21}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
message_id = int(match.group("message_id"))
|
||||
channel_id = match.group("channel_id")
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Message:
|
||||
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
|
||||
message = ctx.bot._connection._get_message(message_id)
|
||||
if message:
|
||||
return message
|
||||
channel = ctx.bot.get_channel(int(channel_id)) if channel_id else ctx.channel
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
try:
|
||||
@ -290,7 +324,7 @@ class MessageConverter(Converter):
|
||||
except discord.Forbidden:
|
||||
raise ChannelNotReadable(channel)
|
||||
|
||||
class TextChannelConverter(IDConverter):
|
||||
class TextChannelConverter(IDConverter[discord.TextChannel]):
|
||||
"""Converts to a :class:`~discord.TextChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -305,7 +339,7 @@ class TextChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
@ -332,7 +366,7 @@ class TextChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class VoiceChannelConverter(IDConverter):
|
||||
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
|
||||
"""Converts to a :class:`~discord.VoiceChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -347,7 +381,7 @@ class VoiceChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
@ -373,7 +407,47 @@ class VoiceChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
class StageChannelConverter(IDConverter[discord.StageChannel]):
|
||||
"""Converts to a :class:`~discord.StageChannel`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.stage_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StageChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StageChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
|
||||
"""Converts to a :class:`~discord.CategoryChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -388,7 +462,7 @@ class CategoryChannelConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
@ -415,7 +489,48 @@ class CategoryChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter):
|
||||
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
|
||||
"""Converts to a :class:`~discord.StoreChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StoreChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StoreChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter[discord.Colour]):
|
||||
"""Converts to a :class:`~discord.Colour`.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
@ -426,33 +541,80 @@ class ColourConverter(Converter):
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- ``rgb(<number>, <number>, <number>)``
|
||||
- Any of the ``classmethod`` in :class:`Colour`
|
||||
|
||||
- The ``_`` in the name can be optionally replaced with spaces.
|
||||
|
||||
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
|
||||
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
arg = argument.replace('0x', '').lower()
|
||||
|
||||
if arg[0] == '#':
|
||||
arg = arg[1:]
|
||||
.. versionchanged:: 1.7
|
||||
Added support for ``rgb`` function and 3-digit hex shortcuts
|
||||
"""
|
||||
|
||||
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)')
|
||||
|
||||
def parse_hex_number(self, argument):
|
||||
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
|
||||
try:
|
||||
value = int(arg, base=16)
|
||||
if not (0 <= value <= 0xFFFFFF):
|
||||
raise BadColourArgument(arg)
|
||||
return discord.Colour(value=value)
|
||||
raise BadColourArgument(argument)
|
||||
except ValueError:
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
raise BadColourArgument(argument)
|
||||
else:
|
||||
return discord.Color(value=value)
|
||||
|
||||
def parse_rgb_number(self, argument, number):
|
||||
if number[-1] == '%':
|
||||
value = int(number[:-1])
|
||||
if not (0 <= value <= 100):
|
||||
raise BadColourArgument(argument)
|
||||
return round(255 * (value / 100))
|
||||
|
||||
value = int(number)
|
||||
if not (0 <= value <= 255):
|
||||
raise BadColourArgument(argument)
|
||||
return value
|
||||
|
||||
def parse_rgb(self, argument, *, regex=RGB_REGEX):
|
||||
match = regex.match(argument)
|
||||
if match is None:
|
||||
raise BadColourArgument(argument)
|
||||
|
||||
red = self.parse_rgb_number(argument, match.group('r'))
|
||||
green = self.parse_rgb_number(argument, match.group('g'))
|
||||
blue = self.parse_rgb_number(argument, match.group('b'))
|
||||
return discord.Color.from_rgb(red, green, blue)
|
||||
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Colour:
|
||||
if argument[0] == '#':
|
||||
return self.parse_hex_number(argument[1:])
|
||||
|
||||
if argument[0:2] == '0x':
|
||||
rest = argument[2:]
|
||||
# Legacy backwards compatible syntax
|
||||
if rest.startswith('#'):
|
||||
return self.parse_hex_number(rest[1:])
|
||||
return self.parse_hex_number(rest)
|
||||
|
||||
arg = argument.lower()
|
||||
if arg[0:3] == 'rgb':
|
||||
return self.parse_rgb(arg)
|
||||
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
|
||||
ColorConverter = ColourConverter
|
||||
|
||||
class RoleConverter(IDConverter):
|
||||
class RoleConverter(IDConverter[discord.Role]):
|
||||
"""Converts to a :class:`~discord.Role`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
@ -467,7 +629,7 @@ class RoleConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Role:
|
||||
guild = ctx.guild
|
||||
if not guild:
|
||||
raise NoPrivateMessage()
|
||||
@ -482,12 +644,12 @@ class RoleConverter(IDConverter):
|
||||
raise RoleNotFound(argument)
|
||||
return result
|
||||
|
||||
class GameConverter(Converter):
|
||||
class GameConverter(Converter[discord.Game]):
|
||||
"""Converts to :class:`~discord.Game`."""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Game:
|
||||
return discord.Game(name=argument)
|
||||
|
||||
class InviteConverter(Converter):
|
||||
class InviteConverter(Converter[discord.Invite]):
|
||||
"""Converts to a :class:`~discord.Invite`.
|
||||
|
||||
This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
|
||||
@ -495,14 +657,40 @@ class InviteConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
|
||||
try:
|
||||
invite = await ctx.bot.fetch_invite(argument)
|
||||
return invite
|
||||
except Exception as exc:
|
||||
raise BadInviteArgument() from exc
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
class GuildConverter(IDConverter[discord.Guild]):
|
||||
"""Converts to a :class:`~discord.Guild`.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Guild:
|
||||
match = self._get_id_match(argument)
|
||||
result = None
|
||||
|
||||
if match is not None:
|
||||
guild_id = int(match.group(1))
|
||||
result = ctx.bot.get_guild(guild_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||
|
||||
if result is None:
|
||||
raise GuildNotFound(argument)
|
||||
return result
|
||||
|
||||
class EmojiConverter(IDConverter[discord.Emoji]):
|
||||
"""Converts to a :class:`~discord.Emoji`.
|
||||
|
||||
All lookups are done for the local guild first, if available. If that lookup
|
||||
@ -517,7 +705,7 @@ class EmojiConverter(IDConverter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
|
||||
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
|
||||
result = None
|
||||
bot = ctx.bot
|
||||
@ -545,7 +733,7 @@ class EmojiConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class PartialEmojiConverter(Converter):
|
||||
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
|
||||
"""Converts to a :class:`~discord.PartialEmoji`.
|
||||
|
||||
This is done by extracting the animated flag, name and ID from the emoji.
|
||||
@ -553,7 +741,7 @@ class PartialEmojiConverter(Converter):
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
|
||||
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
|
||||
|
||||
if match:
|
||||
@ -566,7 +754,7 @@ class PartialEmojiConverter(Converter):
|
||||
|
||||
raise PartialEmojiConversionFailure(argument)
|
||||
|
||||
class clean_content(Converter):
|
||||
class clean_content(Converter[str]):
|
||||
"""Converts the argument to mention scrubbed version of
|
||||
said content.
|
||||
|
||||
@ -580,20 +768,25 @@ class clean_content(Converter):
|
||||
Whether to use nicknames when transforming mentions.
|
||||
escape_markdown: :class:`bool`
|
||||
Whether to also escape special markdown characters.
|
||||
remove_markdown: :class:`bool`
|
||||
Whether to also remove special markdown characters. This option is not supported with ``escape_markdown``
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
|
||||
self.fix_channel_mentions = fix_channel_mentions
|
||||
self.use_nicknames = use_nicknames
|
||||
self.escape_markdown = escape_markdown
|
||||
self.remove_markdown = remove_markdown
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
async def convert(self, ctx: Context, argument: str) -> str:
|
||||
message = ctx.message
|
||||
transformations = {}
|
||||
|
||||
if self.fix_channel_mentions and ctx.guild:
|
||||
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
||||
ch = _get(id)
|
||||
return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel')
|
||||
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
|
||||
|
||||
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
||||
|
||||
@ -608,12 +801,12 @@ class clean_content(Converter):
|
||||
|
||||
|
||||
transformations.update(
|
||||
('<@%s>' % member_id, resolve_member(member_id))
|
||||
(f'<@{member_id}>', resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
transformations.update(
|
||||
('<@!%s>' % member_id, resolve_member(member_id))
|
||||
(f'<@!{member_id}>', resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
@ -623,7 +816,7 @@ class clean_content(Converter):
|
||||
return '@' + r.name if r else '@deleted-role'
|
||||
|
||||
transformations.update(
|
||||
('<@&%s>' % role_id, resolve_role(role_id))
|
||||
(f'<@&{role_id}>', resolve_role(role_id))
|
||||
for role_id in message.raw_role_mentions
|
||||
)
|
||||
|
||||
@ -635,6 +828,8 @@ class clean_content(Converter):
|
||||
|
||||
if self.escape_markdown:
|
||||
result = discord.utils.escape_markdown(result)
|
||||
elif self.remove_markdown:
|
||||
result = discord.utils.remove_markdown(result)
|
||||
|
||||
# Completely ensure no mentions escape:
|
||||
return discord.utils.escape_mentions(result)
|
||||
@ -656,10 +851,10 @@ class _Greedy:
|
||||
raise TypeError('Greedy[...] expects a type or a Converter instance.')
|
||||
|
||||
if converter is str or converter is type(None) or converter is _Greedy:
|
||||
raise TypeError('Greedy[%s] is invalid.' % converter.__name__)
|
||||
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
|
||||
|
||||
if getattr(converter, '__origin__', None) is typing.Union and type(None) in converter.__args__:
|
||||
raise TypeError('Greedy[%r] is invalid.' % converter)
|
||||
if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
|
||||
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
||||
|
||||
return self.__class__(converter=converter)
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -66,6 +64,9 @@ class BucketType(Enum):
|
||||
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
|
||||
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
|
||||
|
||||
def __call__(self, msg):
|
||||
return self.get_key(msg)
|
||||
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
||||
@ -78,8 +79,8 @@ class Cooldown:
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
if not isinstance(self.type, BucketType):
|
||||
raise TypeError('Cooldown type must be a BucketType')
|
||||
if not callable(self.type):
|
||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
if not current:
|
||||
@ -151,7 +152,7 @@ class CooldownMapping:
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
return self._cooldown.type.get_key(msg)
|
||||
return self._cooldown.type(msg)
|
||||
|
||||
def _verify_cache_integrity(self, current=None):
|
||||
# we want to delete all cache objects that haven't been used
|
||||
@ -252,7 +253,7 @@ class MaxConcurrency:
|
||||
raise ValueError('max_concurrency \'number\' cannot be less than 1')
|
||||
|
||||
if not isinstance(per, BucketType):
|
||||
raise TypeError('max_concurrency \'per\' must be of type BucketType not %r' % type(per))
|
||||
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self.number, per=self.per, wait=self.wait)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,7 @@ import functools
|
||||
import inspect
|
||||
import typing
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
import discord
|
||||
|
||||
@ -158,7 +157,7 @@ class Command(_BaseCommand):
|
||||
isn't one.
|
||||
cog: Optional[:class:`Cog`]
|
||||
The cog that this command belongs to. ``None`` if there isn't one.
|
||||
checks: List[Callable[..., :class:`bool`]]
|
||||
checks: List[Callable[[:class:`.Context`], :class:`bool`]]
|
||||
A list of predicates that verifies if the command could be executed
|
||||
with the given :class:`.Context` as the sole parameter. If an exception
|
||||
is necessary to be thrown to signal failure, then one inherited from
|
||||
@ -301,17 +300,37 @@ class Command(_BaseCommand):
|
||||
signature = inspect.signature(function)
|
||||
self.params = signature.parameters.copy()
|
||||
|
||||
# PEP-563 allows postponing evaluation of annotations with a __future__
|
||||
# import. When postponed, Parameter.annotation will be a string and must
|
||||
# be replaced with the real value for the converters to work later on
|
||||
# see: https://bugs.python.org/issue41341
|
||||
resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
|
||||
|
||||
try:
|
||||
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
|
||||
except NameError as e:
|
||||
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
|
||||
|
||||
for key, value in self.params.items():
|
||||
if isinstance(value.annotation, str):
|
||||
self.params[key] = value = value.replace(annotation=eval(value.annotation, function.__globals__))
|
||||
# coalesce the forward references
|
||||
if key in type_hints:
|
||||
self.params[key] = value = value.replace(annotation=type_hints[key])
|
||||
|
||||
# fail early for when someone passes an unparameterized Greedy type
|
||||
if value.annotation is converters.Greedy:
|
||||
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
|
||||
|
||||
def _return_resolved(self, type, **kwargs):
|
||||
return type
|
||||
|
||||
def _recursive_resolve(self, type, *, globals=None):
|
||||
if not isinstance(type, typing.ForwardRef):
|
||||
return type
|
||||
|
||||
resolved = eval(type.__forward_arg__, globals)
|
||||
args = typing.get_args(resolved)
|
||||
for index, arg in enumerate(args):
|
||||
inner_resolve_result = self._recursive_resolve(arg, globals=globals)
|
||||
resolved[index] = inner_resolve_result
|
||||
return resolved
|
||||
|
||||
def add_check(self, func):
|
||||
"""Adds a check to the command.
|
||||
|
||||
@ -445,19 +464,13 @@ class Command(_BaseCommand):
|
||||
converter = getattr(converters, converter.__name__ + 'Converter', converter)
|
||||
|
||||
try:
|
||||
if inspect.isclass(converter):
|
||||
if issubclass(converter, converters.Converter):
|
||||
instance = converter()
|
||||
ret = await instance.convert(ctx, argument)
|
||||
return ret
|
||||
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
|
||||
if inspect.ismethod(converter.convert):
|
||||
return await converter.convert(ctx, argument)
|
||||
else:
|
||||
method = getattr(converter, 'convert', None)
|
||||
if method is not None and inspect.ismethod(method):
|
||||
ret = await method(ctx, argument)
|
||||
return ret
|
||||
return await converter().convert(ctx, argument)
|
||||
elif isinstance(converter, converters.Converter):
|
||||
ret = await converter.convert(ctx, argument)
|
||||
return ret
|
||||
return await converter.convert(ctx, argument)
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
@ -473,7 +486,7 @@ class Command(_BaseCommand):
|
||||
except AttributeError:
|
||||
name = converter.__class__.__name__
|
||||
|
||||
raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from exc
|
||||
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
||||
|
||||
async def do_conversion(self, ctx, converter, argument, param):
|
||||
try:
|
||||
@ -523,7 +536,7 @@ class Command(_BaseCommand):
|
||||
# The greedy converter is simple -- it keeps going until it fails in which case,
|
||||
# it undos the view ready for the next parameter to use instead
|
||||
if type(converter) is converters._Greedy:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
|
||||
@ -693,7 +706,7 @@ class Command(_BaseCommand):
|
||||
raise discord.ClientException(fmt.format(self))
|
||||
|
||||
for name, param in iterator:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
transformed = await self.transform(ctx, param)
|
||||
args.append(transformed)
|
||||
elif param.kind == param.KEYWORD_ONLY:
|
||||
@ -715,9 +728,8 @@ class Command(_BaseCommand):
|
||||
except RuntimeError:
|
||||
break
|
||||
|
||||
if not self.ignore_extra:
|
||||
if not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
if not self.ignore_extra and not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
|
||||
async def call_before_hooks(self, ctx):
|
||||
# now that we're done preparing we can call the pre-command hooks
|
||||
@ -776,7 +788,7 @@ class Command(_BaseCommand):
|
||||
ctx.command = self
|
||||
|
||||
if not await self.can_run(ctx):
|
||||
raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self))
|
||||
raise CheckFailure(f'The check functions for command {self.qualified_name} failed.')
|
||||
|
||||
if self._max_concurrency is not None:
|
||||
await self._max_concurrency.acquire(ctx)
|
||||
@ -904,6 +916,13 @@ class Command(_BaseCommand):
|
||||
self.on_error = coro
|
||||
return coro
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the command has an error handler registered.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return hasattr(self, 'on_error')
|
||||
|
||||
def before_invoke(self, coro):
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
|
||||
@ -1008,23 +1027,23 @@ class Command(_BaseCommand):
|
||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
||||
should_print = param.default if isinstance(param.default, str) else param.default is not None
|
||||
if should_print:
|
||||
result.append('[%s=%s]' % (name, param.default) if not greedy else
|
||||
'[%s=%s]...' % (name, param.default))
|
||||
result.append(f'[{name}={param.default}]' if not greedy else
|
||||
f'[{name}={param.default}]...')
|
||||
continue
|
||||
else:
|
||||
result.append('[%s]' % name)
|
||||
result.append(f'[{name}]')
|
||||
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
if self.require_var_positional:
|
||||
result.append('<%s...>' % name)
|
||||
result.append(f'<{name}...>')
|
||||
else:
|
||||
result.append('[%s...]' % name)
|
||||
result.append(f'[{name}...]')
|
||||
elif greedy:
|
||||
result.append('[%s]...' % name)
|
||||
result.append(f'[{name}]...')
|
||||
elif self._is_typing_optional(param.annotation):
|
||||
result.append('[%s]' % name)
|
||||
result.append(f'[{name}]')
|
||||
else:
|
||||
result.append('<%s>' % name)
|
||||
result.append(f'<{name}>')
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
@ -1056,14 +1075,14 @@ class Command(_BaseCommand):
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
raise DisabledCommand('{0.name} command is disabled'.format(self))
|
||||
raise DisabledCommand(f'{self.name} command is disabled')
|
||||
|
||||
original = ctx.command
|
||||
ctx.command = self
|
||||
|
||||
try:
|
||||
if not await ctx.bot.can_run(ctx):
|
||||
raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self))
|
||||
raise CheckFailure(f'The global check functions for command {self.qualified_name} failed.')
|
||||
|
||||
cog = self.cog
|
||||
if cog is not None:
|
||||
@ -1335,6 +1354,8 @@ class Group(GroupMixin, Command):
|
||||
injected = hooked_wrapped_callback(self, ctx, self.callback)
|
||||
await injected(*ctx.args, **ctx.kwargs)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.invoke(ctx)
|
||||
@ -1373,6 +1394,8 @@ class Group(GroupMixin, Command):
|
||||
if call_hooks:
|
||||
await self.call_after_hooks(ctx)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks)
|
||||
@ -1578,7 +1601,7 @@ def check_any(*checks):
|
||||
try:
|
||||
pred = wrapped.predicate
|
||||
except AttributeError:
|
||||
raise TypeError('%r must be wrapped by commands.check decorator' % wrapped) from None
|
||||
raise TypeError(f'{wrapped!r} must be wrapped by commands.check decorator') from None
|
||||
else:
|
||||
unwrapped.append(pred)
|
||||
|
||||
@ -1766,7 +1789,7 @@ def has_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
ch = ctx.channel
|
||||
@ -1791,7 +1814,7 @@ def bot_has_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
guild = ctx.guild
|
||||
@ -1819,7 +1842,7 @@ def has_guild_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
if not ctx.guild:
|
||||
@ -1844,7 +1867,7 @@ def bot_has_guild_permissions(**perms):
|
||||
|
||||
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
|
||||
if invalid:
|
||||
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
|
||||
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
|
||||
|
||||
def predicate(ctx):
|
||||
if not ctx.guild:
|
||||
@ -1949,8 +1972,11 @@ def cooldown(rate, per, type=BucketType.default):
|
||||
The number of times a command can be used before triggering a cooldown.
|
||||
per: :class:`float`
|
||||
The amount of seconds to wait for a cooldown when it's been triggered.
|
||||
type: :class:`.BucketType`
|
||||
The type of cooldown to have.
|
||||
type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]]
|
||||
The type of cooldown to have. If callable, should return a key for the mapping.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Callables are now supported for custom bucket types.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -45,6 +43,7 @@ __all__ = (
|
||||
'NotOwner',
|
||||
'MessageNotFound',
|
||||
'MemberNotFound',
|
||||
'GuildNotFound',
|
||||
'UserNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelNotReadable',
|
||||
@ -142,7 +141,7 @@ class MissingRequiredArgument(UserInputError):
|
||||
"""
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
super().__init__('{0.name} is a required argument that is missing.'.format(param))
|
||||
super().__init__(f'{param.name} is a required argument that is missing.')
|
||||
|
||||
class TooManyArguments(UserInputError):
|
||||
"""Exception raised when the command was passed too many arguments and its
|
||||
@ -228,7 +227,23 @@ class MemberNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Member "{}" not found.'.format(argument))
|
||||
super().__init__(f'Member "{argument}" not found.')
|
||||
|
||||
class GuildNotFound(BadArgument):
|
||||
"""Exception raised when the guild provided was not found in the bot's cache.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The guild supplied by the called that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__(f'Guild "{argument}" not found.')
|
||||
|
||||
class UserNotFound(BadArgument):
|
||||
"""Exception raised when the user provided was not found in the bot's
|
||||
@ -245,7 +260,7 @@ class UserNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('User "{}" not found.'.format(argument))
|
||||
super().__init__(f'User "{argument}" not found.')
|
||||
|
||||
class MessageNotFound(BadArgument):
|
||||
"""Exception raised when the message provided was not found in the channel.
|
||||
@ -261,7 +276,7 @@ class MessageNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Message "{}" not found.'.format(argument))
|
||||
super().__init__(f'Message "{argument}" not found.')
|
||||
|
||||
class ChannelNotReadable(BadArgument):
|
||||
"""Exception raised when the bot does not have permission to read messages
|
||||
@ -278,7 +293,7 @@ class ChannelNotReadable(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__("Can't read messages in {}.".format(argument.mention))
|
||||
super().__init__(f"Can't read messages in {argument.mention}.")
|
||||
|
||||
class ChannelNotFound(BadArgument):
|
||||
"""Exception raised when the bot can not find the channel.
|
||||
@ -294,7 +309,7 @@ class ChannelNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Channel "{}" not found.'.format(argument))
|
||||
super().__init__(f'Channel "{argument}" not found.')
|
||||
|
||||
class BadColourArgument(BadArgument):
|
||||
"""Exception raised when the colour is not valid.
|
||||
@ -310,7 +325,7 @@ class BadColourArgument(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Colour "{}" is invalid.'.format(argument))
|
||||
super().__init__(f'Colour "{argument}" is invalid.')
|
||||
|
||||
BadColorArgument = BadColourArgument
|
||||
|
||||
@ -328,7 +343,7 @@ class RoleNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Role "{}" not found.'.format(argument))
|
||||
super().__init__(f'Role "{argument}" not found.')
|
||||
|
||||
class BadInviteArgument(BadArgument):
|
||||
"""Exception raised when the invite is invalid or expired.
|
||||
@ -354,7 +369,7 @@ class EmojiNotFound(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Emoji "{}" not found.'.format(argument))
|
||||
super().__init__(f'Emoji "{argument}" not found.')
|
||||
|
||||
class PartialEmojiConversionFailure(BadArgument):
|
||||
"""Exception raised when the emoji provided does not match the correct
|
||||
@ -371,7 +386,7 @@ class PartialEmojiConversionFailure(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
||||
super().__init__(f'Couldn\'t convert "{argument}" to PartialEmoji.')
|
||||
|
||||
class BadBoolArgument(BadArgument):
|
||||
"""Exception raised when a boolean argument was not convertable.
|
||||
@ -387,7 +402,7 @@ class BadBoolArgument(BadArgument):
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('{} is not a recognised boolean option'.format(argument))
|
||||
super().__init__(f'{argument} is not a recognised boolean option')
|
||||
|
||||
class DisabledCommand(CommandError):
|
||||
"""Exception raised when the command being invoked is disabled.
|
||||
@ -427,7 +442,7 @@ class CommandOnCooldown(CommandError):
|
||||
def __init__(self, cooldown, retry_after):
|
||||
self.cooldown = cooldown
|
||||
self.retry_after = retry_after
|
||||
super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after))
|
||||
super().__init__(f'You are on cooldown. Try again in {retry_after:.2f}s')
|
||||
|
||||
class MaxConcurrencyReached(CommandError):
|
||||
"""Exception raised when the command being invoked has reached its maximum concurrency.
|
||||
@ -449,7 +464,7 @@ class MaxConcurrencyReached(CommandError):
|
||||
suffix = 'per %s' % name if per.name != 'default' else 'globally'
|
||||
plural = '%s times %s' if number > 1 else '%s time %s'
|
||||
fmt = plural % (number, suffix)
|
||||
super().__init__('Too many people using this command. It can only be used {} concurrently.'.format(fmt))
|
||||
super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
|
||||
|
||||
class MissingRole(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks a role to run a command.
|
||||
@ -466,7 +481,7 @@ class MissingRole(CheckFailure):
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Role {0!r} is required to run this command.'.format(missing_role)
|
||||
message = f'Role {missing_role!r} is required to run this command.'
|
||||
super().__init__(message)
|
||||
|
||||
class BotMissingRole(CheckFailure):
|
||||
@ -484,7 +499,7 @@ class BotMissingRole(CheckFailure):
|
||||
"""
|
||||
def __init__(self, missing_role):
|
||||
self.missing_role = missing_role
|
||||
message = 'Bot requires the role {0!r} to run this command'.format(missing_role)
|
||||
message = f'Bot requires the role {missing_role!r} to run this command'
|
||||
super().__init__(message)
|
||||
|
||||
class MissingAnyRole(CheckFailure):
|
||||
@ -504,14 +519,14 @@ class MissingAnyRole(CheckFailure):
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
missing = [f"'{role}'" for role in missing_roles]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' or '.join(missing)
|
||||
|
||||
message = "You are missing at least one of the required roles: {}".format(fmt)
|
||||
message = f"You are missing at least one of the required roles: {fmt}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@ -533,14 +548,14 @@ class BotMissingAnyRole(CheckFailure):
|
||||
def __init__(self, missing_roles):
|
||||
self.missing_roles = missing_roles
|
||||
|
||||
missing = ["'{}'".format(role) for role in missing_roles]
|
||||
missing = [f"'{role}'" for role in missing_roles]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' or '.join(missing)
|
||||
|
||||
message = "Bot is missing at least one of the required roles: {}".format(fmt)
|
||||
message = f"Bot is missing at least one of the required roles: {fmt}"
|
||||
super().__init__(message)
|
||||
|
||||
class NSFWChannelRequired(CheckFailure):
|
||||
@ -557,7 +572,7 @@ class NSFWChannelRequired(CheckFailure):
|
||||
"""
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
super().__init__("Channel '{}' needs to be NSFW for this command to work.".format(channel))
|
||||
super().__init__(f"Channel '{channel}' needs to be NSFW for this command to work.")
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks permissions to run a
|
||||
@ -579,7 +594,7 @@ class MissingPermissions(CheckFailure):
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'You are missing {} permission(s) to run this command.'.format(fmt)
|
||||
message = f'You are missing {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
@ -602,7 +617,7 @@ class BotMissingPermissions(CheckFailure):
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'Bot requires {} permission(s) to run this command.'.format(fmt)
|
||||
message = f'Bot requires {fmt} permission(s) to run this command.'
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BadUnionArgument(UserInputError):
|
||||
@ -637,7 +652,7 @@ class BadUnionArgument(UserInputError):
|
||||
else:
|
||||
fmt = ' or '.join(to_string)
|
||||
|
||||
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
|
||||
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
|
||||
|
||||
class ArgumentParsingError(UserInputError):
|
||||
"""An exception raised when the parser fails to parse a user's input.
|
||||
@ -661,7 +676,7 @@ class UnexpectedQuoteError(ArgumentParsingError):
|
||||
"""
|
||||
def __init__(self, quote):
|
||||
self.quote = quote
|
||||
super().__init__('Unexpected quote mark, {0!r}, in non-quoted string'.format(quote))
|
||||
super().__init__(f'Unexpected quote mark, {quote!r}, in non-quoted string')
|
||||
|
||||
class InvalidEndOfQuotedStringError(ArgumentParsingError):
|
||||
"""An exception raised when a space is expected after the closing quote in a string
|
||||
@ -676,7 +691,7 @@ class InvalidEndOfQuotedStringError(ArgumentParsingError):
|
||||
"""
|
||||
def __init__(self, char):
|
||||
self.char = char
|
||||
super().__init__('Expected space after closing quotation but received {0!r}'.format(char))
|
||||
super().__init__(f'Expected space after closing quotation but received {char!r}')
|
||||
|
||||
class ExpectedClosingQuoteError(ArgumentParsingError):
|
||||
"""An exception raised when a quote character is expected but not found.
|
||||
@ -691,7 +706,7 @@ class ExpectedClosingQuoteError(ArgumentParsingError):
|
||||
|
||||
def __init__(self, close_quote):
|
||||
self.close_quote = close_quote
|
||||
super().__init__('Expected closing {}.'.format(close_quote))
|
||||
super().__init__(f'Expected closing {close_quote}.')
|
||||
|
||||
class ExtensionError(DiscordException):
|
||||
"""Base exception for extension related errors.
|
||||
@ -705,7 +720,7 @@ class ExtensionError(DiscordException):
|
||||
"""
|
||||
def __init__(self, message=None, *args, name):
|
||||
self.name = name
|
||||
message = message or 'Extension {!r} had an error.'.format(name)
|
||||
message = message or f'Extension {name!r} had an error.'
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
|
||||
super().__init__(m, *args)
|
||||
@ -716,7 +731,7 @@ class ExtensionAlreadyLoaded(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} is already loaded.'.format(name), name=name)
|
||||
super().__init__(f'Extension {name!r} is already loaded.', name=name)
|
||||
|
||||
class ExtensionNotLoaded(ExtensionError):
|
||||
"""An exception raised when an extension was not loaded.
|
||||
@ -724,7 +739,7 @@ class ExtensionNotLoaded(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__('Extension {!r} has not been loaded.'.format(name), name=name)
|
||||
super().__init__(f'Extension {name!r} has not been loaded.', name=name)
|
||||
|
||||
class NoEntryPointError(ExtensionError):
|
||||
"""An exception raised when an extension does not have a ``setup`` entry point function.
|
||||
@ -732,7 +747,7 @@ class NoEntryPointError(ExtensionError):
|
||||
This inherits from :exc:`ExtensionError`
|
||||
"""
|
||||
def __init__(self, name):
|
||||
super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name)
|
||||
super().__init__(f"Extension {name!r} has no 'setup' function.", name=name)
|
||||
|
||||
class ExtensionFailed(ExtensionError):
|
||||
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
|
||||
@ -791,4 +806,4 @@ class CommandRegistrationError(ClientException):
|
||||
self.name = name
|
||||
self.alias_conflict = alias_conflict
|
||||
type_ = 'alias' if alias_conflict else 'command'
|
||||
super().__init__('The {} {} is already an existing command or alias.'.format(type_, name))
|
||||
super().__init__(f'The {type_} {name} is already an existing command or alias.')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -79,18 +77,22 @@ class Paginator:
|
||||
The suffix appended at the end of every page. e.g. three backticks.
|
||||
max_size: :class:`int`
|
||||
The maximum amount of codepoints allowed in a page.
|
||||
linesep: :class:`str`
|
||||
The character string inserted between lines. e.g. a newline character.
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000):
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size
|
||||
self.linesep = linesep
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
"""Clears the paginator to have no pages."""
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@ -104,6 +106,10 @@ class Paginator:
|
||||
def _suffix_len(self):
|
||||
return len(self.suffix) if self.suffix else 0
|
||||
|
||||
@property
|
||||
def _linesep_len(self):
|
||||
return len(self.linesep)
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
"""Adds a line to the current page.
|
||||
|
||||
@ -122,29 +128,29 @@ class Paginator:
|
||||
RuntimeError
|
||||
The line was too big for the current :attr:`max_size`.
|
||||
"""
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
|
||||
if len(line) > max_page_size:
|
||||
raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
|
||||
raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
|
||||
|
||||
if self._count + len(line) + 1 > self.max_size - self._suffix_len:
|
||||
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
|
||||
self.close_page()
|
||||
|
||||
self._count += len(line) + 1
|
||||
self._count += len(line) + self._linesep_len
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._count += 1
|
||||
self._count += self._linesep_len
|
||||
|
||||
def close_page(self):
|
||||
"""Prematurely terminate a page."""
|
||||
if self.suffix is not None:
|
||||
self._current_page.append(self.suffix)
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
self._pages.append(self.linesep.join(self._current_page))
|
||||
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@ -162,7 +168,7 @@ class Paginator:
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
|
||||
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
|
||||
return fmt.format(self)
|
||||
|
||||
def _not_overriden(f):
|
||||
@ -264,9 +270,13 @@ class HelpCommand:
|
||||
show_hidden: :class:`bool`
|
||||
Specifies if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
verify_checks: :class:`bool`
|
||||
verify_checks: Optional[:class:`bool`]
|
||||
Specifies if commands should have their :attr:`.Command.checks` called
|
||||
and verified. Defaults to ``True``.
|
||||
and verified. If ``True``, always calls :attr:`.Commands.checks`.
|
||||
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
|
||||
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
|
||||
|
||||
..versionchanged:: 1.7
|
||||
command_attrs: :class:`dict`
|
||||
A dictionary of options to pass in for the construction of the help command.
|
||||
This allows you to change the command behaviour without actually changing
|
||||
@ -309,7 +319,7 @@ class HelpCommand:
|
||||
attrs.setdefault('name', 'help')
|
||||
attrs.setdefault('help', 'Shows this message')
|
||||
self.context = None
|
||||
self._command_impl = None
|
||||
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
|
||||
|
||||
def copy(self):
|
||||
obj = self.__class__(*self.__original_args__, **self.__original_kwargs__)
|
||||
@ -324,7 +334,6 @@ class HelpCommand:
|
||||
def _remove_from_bot(self, bot):
|
||||
bot.remove_command(self._command_impl.name)
|
||||
self._command_impl._eject_cog()
|
||||
self._command_impl = None
|
||||
|
||||
def add_check(self, func):
|
||||
"""
|
||||
@ -338,13 +347,7 @@ class HelpCommand:
|
||||
The function that will be used as a check.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.add_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].append(func)
|
||||
except KeyError:
|
||||
self.command_attrs["checks"] = [func]
|
||||
self._command_impl.add_check(func)
|
||||
|
||||
def remove_check(self, func):
|
||||
"""
|
||||
@ -361,13 +364,7 @@ class HelpCommand:
|
||||
The function to remove from the checks.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.remove_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].remove(func)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
self._command_impl.remove_check(func)
|
||||
|
||||
def get_bot_mapping(self):
|
||||
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
||||
@ -376,7 +373,7 @@ class HelpCommand:
|
||||
cog: cog.get_commands()
|
||||
for cog in bot.cogs.values()
|
||||
}
|
||||
mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
|
||||
mapping[None] = [c for c in bot.commands if c.cog is None]
|
||||
return mapping
|
||||
|
||||
@property
|
||||
@ -387,8 +384,9 @@ class HelpCommand:
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
# for this common use case rather than waste performance for the
|
||||
# odd one.
|
||||
pattern = re.compile(r"<@!?%s>" % user.id)
|
||||
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.context.prefix)
|
||||
pattern = re.compile(fr"<@!?{user.id}>")
|
||||
display_name = user.display_name.replace('\\', r'\\')
|
||||
return pattern.sub('@' + display_name, self.context.prefix)
|
||||
|
||||
@property
|
||||
def invoked_with(self):
|
||||
@ -425,17 +423,26 @@ class HelpCommand:
|
||||
The signature for the command.
|
||||
"""
|
||||
|
||||
parent = command.full_parent_name
|
||||
parent = command.parent
|
||||
entries = []
|
||||
while parent is not None:
|
||||
if not parent.signature or parent.invoke_without_command:
|
||||
entries.append(parent.name)
|
||||
else:
|
||||
entries.append(parent.name + ' ' + parent.signature)
|
||||
parent = parent.parent
|
||||
parent_sig = ' '.join(reversed(entries))
|
||||
|
||||
if len(command.aliases) > 0:
|
||||
aliases = '|'.join(command.aliases)
|
||||
fmt = '[%s|%s]' % (command.name, aliases)
|
||||
if parent:
|
||||
fmt = parent + ' ' + fmt
|
||||
fmt = f'[{command.name}|{aliases}]'
|
||||
if parent_sig:
|
||||
fmt = parent_sig + ' ' + fmt
|
||||
alias = fmt
|
||||
else:
|
||||
alias = command.name if not parent else parent + ' ' + command.name
|
||||
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
||||
|
||||
return '%s%s %s' % (self.clean_prefix, alias, command.signature)
|
||||
return f'{self.clean_prefix}{alias} {command.signature}'
|
||||
|
||||
def remove_mentions(self, string):
|
||||
"""Removes mentions from the string to prevent abuse.
|
||||
@ -498,7 +505,7 @@ class HelpCommand:
|
||||
:class:`str`
|
||||
The string to use when a command has not been found.
|
||||
"""
|
||||
return 'No command called "{}" found.'.format(string)
|
||||
return f'No command called "{string}" found.'
|
||||
|
||||
def subcommand_not_found(self, command, string):
|
||||
"""|maybecoro|
|
||||
@ -527,8 +534,8 @@ class HelpCommand:
|
||||
The string to use when the command did not have the subcommand requested.
|
||||
"""
|
||||
if isinstance(command, Group) and len(command.all_commands) > 0:
|
||||
return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string)
|
||||
return 'Command "{0.qualified_name}" has no subcommands.'.format(command)
|
||||
return f'Command "{command.qualified_name}" has no subcommand named {string}'
|
||||
return f'Command "{command.qualified_name}" has no subcommands.'
|
||||
|
||||
async def filter_commands(self, commands, *, sort=False, key=None):
|
||||
"""|coro|
|
||||
@ -560,11 +567,15 @@ class HelpCommand:
|
||||
|
||||
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
|
||||
|
||||
if not self.verify_checks:
|
||||
if self.verify_checks is False:
|
||||
# if we do not need to verify the checks then we can just
|
||||
# run it straight through normally without using await.
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
if self.verify_checks is None and not self.context.guild:
|
||||
# if verify_checks is None and we're in a DM, don't verify
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
# if we're here then we need to check every command if it can run
|
||||
async def predicate(cmd):
|
||||
try:
|
||||
@ -929,8 +940,8 @@ class DefaultHelpCommand(HelpCommand):
|
||||
def get_ending_note(self):
|
||||
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||
command_name = self.invoked_with
|
||||
return "Type {0}{1} command for more info on a command.\n" \
|
||||
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
|
||||
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
|
||||
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
|
||||
|
||||
def add_indented_commands(self, commands, *, heading, max_size=None):
|
||||
"""Indents a list of commands after the specified heading.
|
||||
@ -965,7 +976,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
for command in commands:
|
||||
name = command.name
|
||||
width = max_size - (get_width(name) - len(name))
|
||||
entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width)
|
||||
entry = f'{self.indent * " "}{name:<{width}} {command.short_doc}'
|
||||
self.paginator.add_line(self.shorten_text(entry))
|
||||
|
||||
async def send_pages(self):
|
||||
@ -1018,7 +1029,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
# <description> portion
|
||||
self.paginator.add_line(bot.description, empty=True)
|
||||
|
||||
no_category = '\u200b{0.no_category}:'.format(self)
|
||||
no_category = f'\u200b{self.no_category}:'
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name + ':' if cog is not None else no_category
|
||||
@ -1142,7 +1153,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
|
||||
|
||||
def get_command_signature(self, command):
|
||||
return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
|
||||
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
|
||||
|
||||
def get_ending_note(self):
|
||||
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
||||
@ -1174,7 +1185,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if commands:
|
||||
# U+2002 Middle Dot
|
||||
joined = '\u2002'.join(c.name for c in commands)
|
||||
self.paginator.add_line('__**%s**__' % heading)
|
||||
self.paginator.add_line(f'__**{heading}**__')
|
||||
self.paginator.add_line(joined)
|
||||
|
||||
def add_subcommand_formatting(self, command):
|
||||
@ -1208,7 +1219,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
aliases: Sequence[:class:`str`]
|
||||
A list of aliases to format.
|
||||
"""
|
||||
self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True)
|
||||
self.paginator.add_line(f'**{self.aliases_heading}** {", ".join(aliases)}', empty=True)
|
||||
|
||||
def add_command_formatting(self, command):
|
||||
"""A utility function to format commands and groups.
|
||||
@ -1261,7 +1272,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
no_category = '\u200b{0.no_category}'.format(self)
|
||||
no_category = f'\u200b{self.no_category}'
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name if cog is not None else no_category
|
||||
@ -1294,7 +1305,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
|
||||
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
|
||||
if filtered:
|
||||
self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading))
|
||||
self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
@ -1314,7 +1325,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
self.paginator.add_line('**%s**' % self.commands_heading)
|
||||
self.paginator.add_line(f'**{self.commands_heading}**')
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -37,6 +35,10 @@ from discord.backoff import ExponentialBackoff
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = (
|
||||
'loop',
|
||||
)
|
||||
|
||||
class Loop:
|
||||
"""A background task helper that abstracts the loop and reconnection logic for you.
|
||||
|
||||
@ -103,7 +105,7 @@ class Loop:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if now > self._next_iteration:
|
||||
self._next_iteration = now
|
||||
except self._valid_exception as exc:
|
||||
except self._valid_exception:
|
||||
self._last_iteration_failed = True
|
||||
if not self.reconnect:
|
||||
raise
|
||||
@ -154,14 +156,14 @@ class Loop:
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._task is None and self._sleep:
|
||||
if self._task is None:
|
||||
return None
|
||||
elif self._task and self._task.done() or self._stop_next_iteration:
|
||||
return None
|
||||
return self._next_iteration
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
"""|coro|
|
||||
r"""|coro|
|
||||
|
||||
Calls the internal callback that the task holds.
|
||||
|
||||
@ -289,9 +291,9 @@ class Loop:
|
||||
|
||||
for exc in exceptions:
|
||||
if not inspect.isclass(exc):
|
||||
raise TypeError('{0!r} must be a class.'.format(exc))
|
||||
raise TypeError(f'{exc!r} must be a class.')
|
||||
if not issubclass(exc, BaseException):
|
||||
raise TypeError('{0!r} must inherit from BaseException.'.format(exc))
|
||||
raise TypeError(f'{exc!r} must inherit from BaseException.')
|
||||
|
||||
self._valid_exception = (*self._valid_exception, *exceptions)
|
||||
|
||||
@ -345,7 +347,7 @@ class Loop:
|
||||
|
||||
async def _error(self, *args):
|
||||
exception = args[-1]
|
||||
print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr)
|
||||
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
def before_loop(self, coro):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
import os.path
|
||||
import io
|
||||
|
||||
__all__ = (
|
||||
'File',
|
||||
)
|
||||
|
||||
class File:
|
||||
r"""A parameter object used for :meth:`abc.Messageable.send`
|
||||
for sending file objects.
|
||||
@ -58,14 +60,14 @@ class File:
|
||||
Whether the attachment is a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = ('fp', 'filename', '_original_pos', '_owner', '_closer')
|
||||
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
|
||||
|
||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
||||
self.fp = fp
|
||||
|
||||
if isinstance(fp, io.IOBase):
|
||||
if not (fp.seekable() and fp.readable()):
|
||||
raise ValueError('File buffer {!r} must be seekable and readable'.format(fp))
|
||||
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
|
||||
self.fp = fp
|
||||
self._original_pos = fp.tell()
|
||||
self._owner = False
|
||||
@ -92,6 +94,8 @@ class File:
|
||||
if spoiler and self.filename is not None and not self.filename.startswith('SPOILER_'):
|
||||
self.filename = 'SPOILER_' + self.filename
|
||||
|
||||
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
|
||||
|
||||
def reset(self, *, seek=True):
|
||||
# The `seek` parameter is needed because
|
||||
# the retry-loop is iterated over multiple times
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, ClassVar, Dict, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, overload
|
||||
|
||||
from .enums import UserFlags
|
||||
|
||||
__all__ = (
|
||||
@ -34,27 +36,38 @@ __all__ = (
|
||||
'MemberCacheFlags',
|
||||
)
|
||||
|
||||
class flag_value:
|
||||
def __init__(self, func):
|
||||
FV = TypeVar('FV', bound='flag_value')
|
||||
BF = TypeVar('BF', bound='BaseFlags')
|
||||
|
||||
class flag_value(Generic[BF]):
|
||||
def __init__(self, func: Callable[[Any], int]):
|
||||
self.flag = func(None)
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
@overload
|
||||
def __get__(self: FV, instance: None, owner: Type[BF]) -> FV:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: BF, owner: Type[BF]) -> bool:
|
||||
...
|
||||
|
||||
def __get__(self, instance: Optional[BF], owner: Type[BF]) -> Any:
|
||||
if instance is None:
|
||||
return self
|
||||
return instance._has_flag(self.flag)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
def __set__(self, instance: BF, value: bool) -> None:
|
||||
instance._set_flag(self.flag, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<flag_value flag={.flag!r}>'.format(self)
|
||||
return f'<flag_value flag={self.flag!r}>'
|
||||
|
||||
class alias_flag_value(flag_value):
|
||||
pass
|
||||
|
||||
def fill_with_flags(*, inverted=False):
|
||||
def decorator(cls):
|
||||
def fill_with_flags(*, inverted: bool = False):
|
||||
def decorator(cls: Type[BF]):
|
||||
cls.VALID_FLAGS = {
|
||||
name: value.flag
|
||||
for name, value in cls.__dict__.items()
|
||||
@ -72,13 +85,18 @@ def fill_with_flags(*, inverted=False):
|
||||
|
||||
# n.b. flags must inherit from this and use the decorator above
|
||||
class BaseFlags:
|
||||
VALID_FLAGS: ClassVar[Dict[str, int]]
|
||||
DEFAULT_VALUE: ClassVar[int]
|
||||
|
||||
value: int
|
||||
|
||||
__slots__ = ('value',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@ -87,19 +105,19 @@ class BaseFlags:
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, self.__class__) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s value=%s>' % (self.__class__.__name__, self.value)
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} value={self.value}>'
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tuple[str, bool]]:
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, alias_flag_value):
|
||||
continue
|
||||
@ -107,16 +125,16 @@ class BaseFlags:
|
||||
if isinstance(value, flag_value):
|
||||
yield (name, self._has_flag(value.flag))
|
||||
|
||||
def _has_flag(self, o):
|
||||
def _has_flag(self, o: int) -> bool:
|
||||
return (self.value & o) == o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
def _set_flag(self, o: int, toggle: bool) -> None:
|
||||
if toggle is True:
|
||||
self.value |= o
|
||||
elif toggle is False:
|
||||
self.value &= ~o
|
||||
else:
|
||||
raise TypeError('Value to set for %s must be a bool.' % self.__class__.__name__)
|
||||
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
|
||||
|
||||
@fill_with_flags(inverted=True)
|
||||
class SystemChannelFlags(BaseFlags):
|
||||
@ -152,6 +170,7 @@ class SystemChannelFlags(BaseFlags):
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
# For some reason the flags for system channels are "inverted"
|
||||
@ -159,10 +178,10 @@ class SystemChannelFlags(BaseFlags):
|
||||
# Since this is counter-intuitive from an API perspective and annoying
|
||||
# these will be inverted automatically
|
||||
|
||||
def _has_flag(self, o):
|
||||
def _has_flag(self, o: int) -> bool:
|
||||
return (self.value & o) != o
|
||||
|
||||
def _set_flag(self, o, toggle):
|
||||
def _set_flag(self, o: int, toggle: bool) -> None:
|
||||
if toggle is True:
|
||||
self.value &= ~o
|
||||
elif toggle is False:
|
||||
@ -212,6 +231,7 @@ class MessageFlags(BaseFlags):
|
||||
representing the currently available flags. You should query
|
||||
flags via the properties rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@flag_value
|
||||
@ -348,7 +368,7 @@ class PublicUserFlags(BaseFlags):
|
||||
"""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
def all(self):
|
||||
def all(self) -> List[UserFlags]:
|
||||
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
|
||||
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
|
||||
|
||||
@ -395,11 +415,11 @@ class Intents(BaseFlags):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@ -419,7 +439,7 @@ class Intents(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
@ -428,14 +448,14 @@ class Intents(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything disabled."""
|
||||
self = cls.__new__(cls)
|
||||
self.value = self.DEFAULT_VALUE
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
def default(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||
except :attr:`presences` and :attr:`members`.
|
||||
"""
|
||||
@ -492,7 +512,7 @@ class Intents(BaseFlags):
|
||||
- :attr:`Member.nick`
|
||||
- :attr:`Member.premium_since`
|
||||
- :attr:`User.name`
|
||||
- :attr:`User.avatar` (:meth:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :attr:`User.discriminator`
|
||||
|
||||
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
||||
@ -844,16 +864,16 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: bool):
|
||||
bits = max(self.VALID_FLAGS.values()).bit_length()
|
||||
self.value = (1 << bits) - 1
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything enabled."""
|
||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
value = (1 << bits) - 1
|
||||
@ -862,7 +882,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[MemberCacheFlags]) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` with everything disabled."""
|
||||
self = cls.__new__(cls)
|
||||
self.value = self.DEFAULT_VALUE
|
||||
@ -905,7 +925,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def from_intents(cls, intents):
|
||||
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` based on
|
||||
the currently selected :class:`Intents`.
|
||||
|
||||
@ -933,7 +953,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
return self
|
||||
|
||||
def _verify_intents(self, intents):
|
||||
def _verify_intents(self, intents: Intents):
|
||||
if self.online and not intents.presences:
|
||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
||||
|
||||
@ -954,4 +974,4 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
@property
|
||||
def _online_only(self):
|
||||
return self.value == 1
|
||||
return self.value == 1
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -162,8 +160,8 @@ class KeepAliveHandler(threading.Thread):
|
||||
except KeyError:
|
||||
msg = self.block_msg
|
||||
else:
|
||||
stack = traceback.format_stack(frame)
|
||||
msg = '%s\nLoop thread traceback (most recent call last):\n%s' % (self.block_msg, ''.join(stack))
|
||||
stack = ''.join(traceback.format_stack(frame))
|
||||
msg = f'{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}'
|
||||
log.warning(msg, self.shard_id, total)
|
||||
|
||||
except Exception:
|
||||
@ -380,9 +378,6 @@ class DiscordWebSocket:
|
||||
}
|
||||
}
|
||||
|
||||
if not self._connection.is_bot:
|
||||
payload['d']['synced_guilds'] = []
|
||||
|
||||
if self.shard_id is not None and self.shard_count is not None:
|
||||
payload['d']['shard'] = [self.shard_id, self.shard_count]
|
||||
|
||||
@ -422,16 +417,11 @@ class DiscordWebSocket:
|
||||
if type(msg) is bytes:
|
||||
self._buffer.extend(msg)
|
||||
|
||||
if len(msg) >= 4:
|
||||
if msg[-4:] == b'\x00\x00\xff\xff':
|
||||
msg = self._zlib.decompress(self._buffer)
|
||||
msg = msg.decode('utf-8')
|
||||
self._buffer = bytearray()
|
||||
else:
|
||||
return
|
||||
else:
|
||||
if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff':
|
||||
return
|
||||
|
||||
msg = self._zlib.decompress(self._buffer)
|
||||
msg = msg.decode('utf-8')
|
||||
self._buffer = bytearray()
|
||||
msg = json.loads(msg)
|
||||
|
||||
log.debug('For Shard ID %s: WebSocket Event: %s', self.shard_id, msg)
|
||||
@ -629,13 +619,6 @@ class DiscordWebSocket:
|
||||
log.debug('Sending "%s" to change status', sent)
|
||||
await self.send(sent)
|
||||
|
||||
async def request_sync(self, guild_ids):
|
||||
payload = {
|
||||
'op': self.GUILD_SYNC,
|
||||
'd': list(guild_ids)
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None):
|
||||
payload = {
|
||||
'op': self.REQUEST_MEMBERS,
|
||||
@ -876,7 +859,7 @@ class DiscordVoiceWebSocket:
|
||||
def average_latency(self):
|
||||
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
||||
heartbeat = self._keep_alive
|
||||
if heartbeat is None:
|
||||
if heartbeat is None or not heartbeat.recent_ack_latencies:
|
||||
return float('inf')
|
||||
|
||||
return sum(heartbeat.recent_ack_latencies) / len(heartbeat.recent_ack_latencies)
|
||||
|
207
discord/guild.py
207
discord/guild.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -46,6 +44,9 @@ from .asset import Asset
|
||||
from .flags import SystemChannelFlags
|
||||
from .integrations import Integration
|
||||
|
||||
__all__ = (
|
||||
'Guild',
|
||||
)
|
||||
|
||||
BanEntry = namedtuple('BanEntry', 'reason user')
|
||||
_GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize')
|
||||
@ -205,18 +206,21 @@ class Guild(Hashable):
|
||||
self._members.pop(member.id, None)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name or ''
|
||||
|
||||
def __int__(self):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
'id', 'name', 'shard_id', 'chunked'
|
||||
('id', self.id),
|
||||
('name', self.name),
|
||||
('shard_id', self.shard_id),
|
||||
('chunked', self.chunked),
|
||||
('member_count', getattr(self, '_member_count', None)),
|
||||
)
|
||||
resolved = ['%s=%r' % (attr, getattr(self, attr)) for attr in attrs]
|
||||
resolved.append('member_count=%r' % getattr(self, '_member_count', None))
|
||||
return '<Guild %s>' % ' '.join(resolved)
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<Guild {inner}>'
|
||||
|
||||
def _update_voice_state(self, data, channel_id):
|
||||
user_id = int(data['user_id'])
|
||||
@ -374,6 +378,18 @@ class Guild(Hashable):
|
||||
r.sort(key=lambda c: (c.position, c.id))
|
||||
return r
|
||||
|
||||
@property
|
||||
def stage_channels(self):
|
||||
"""List[:class:`StageChannel`]: A list of voice channels that belongs to this guild.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
This is sorted by the position and are in UI order from top to bottom.
|
||||
"""
|
||||
r = [ch for ch in self._channels.values() if isinstance(ch, StageChannel)]
|
||||
r.sort(key=lambda c: (c.position, c.id))
|
||||
return r
|
||||
|
||||
@property
|
||||
def me(self):
|
||||
""":class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`.
|
||||
@ -861,6 +877,13 @@ class Guild(Hashable):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
rtc_region = options.pop('rtc_region')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
||||
|
||||
parent_id = category.id if category else None
|
||||
return self._state.http.create_channel(self.id, channel_type.value, name=name, parent_id=parent_id,
|
||||
permission_overwrites=perms, **options)
|
||||
@ -962,6 +985,11 @@ class Guild(Hashable):
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a voice channel.
|
||||
rtc_region: Optional[:class:`VoiceRegion`]
|
||||
The region for the voice channel's voice communication.
|
||||
A value of ``None`` indicates automatic voice region detection.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -984,6 +1012,38 @@ class Guild(Hashable):
|
||||
self._channels[channel.id] = channel
|
||||
return channel
|
||||
|
||||
async def create_stage_channel(self, name, *, topic=None, category=None, overwrites=None, reason=None, position=None):
|
||||
"""|coro|
|
||||
|
||||
This is similar to :meth:`create_text_channel` except makes a :class:`StageChannel` instead.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``slowmode_delay`` and ``nsfw`` parameters are not supported in this function.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have the proper permissions to create this channel.
|
||||
HTTPException
|
||||
Creating the channel failed.
|
||||
InvalidArgument
|
||||
The permission overwrite information is not in proper form.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`StageChannel`
|
||||
The channel that was just created.
|
||||
"""
|
||||
data = await self._create_channel(name, overwrites, ChannelType.stage_voice, category, reason=reason, position=position, topic=topic)
|
||||
channel = StageChannel(state=self._state, guild=self, data=data)
|
||||
|
||||
# temporarily add to the cache
|
||||
self._channels[channel.id] = channel
|
||||
return channel
|
||||
|
||||
async def create_category(self, name, *, overwrites=None, reason=None, position=None):
|
||||
"""|coro|
|
||||
|
||||
@ -1278,9 +1338,7 @@ class Guild(Hashable):
|
||||
return [convert(d) for d in data]
|
||||
|
||||
def fetch_members(self, *, limit=1000, after=None):
|
||||
"""|coro|
|
||||
|
||||
Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this,
|
||||
"""Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this,
|
||||
:meth:`Intents.members` must be enabled.
|
||||
|
||||
.. note::
|
||||
@ -1298,7 +1356,8 @@ class Guild(Hashable):
|
||||
Pass ``None`` to fetch all members. Note that this is potentially slow.
|
||||
after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve members after this date or object.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -1370,7 +1429,7 @@ class Guild(Hashable):
|
||||
async def fetch_member(self, member_id):
|
||||
"""|coro|
|
||||
|
||||
Retreives a :class:`Member` from a guild ID, and a member ID.
|
||||
Retrieves a :class:`Member` from a guild ID, and a member ID.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -1399,9 +1458,7 @@ class Guild(Hashable):
|
||||
async def fetch_ban(self, user):
|
||||
"""|coro|
|
||||
|
||||
Retrieves the :class:`BanEntry` for a user, which is a namedtuple
|
||||
with a ``user`` and ``reason`` field. See :meth:`bans` for more
|
||||
information.
|
||||
Retrieves the :class:`BanEntry` for a user.
|
||||
|
||||
You must have the :attr:`~Permissions.ban_members` permission
|
||||
to get this information.
|
||||
@ -1422,8 +1479,8 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
-------
|
||||
BanEntry
|
||||
The BanEntry object for the specified user.
|
||||
:class:`BanEntry`
|
||||
The :class:`BanEntry` object for the specified user.
|
||||
"""
|
||||
data = await self._state.http.get_ban(user.id, self.id)
|
||||
return BanEntry(
|
||||
@ -1434,12 +1491,7 @@ class Guild(Hashable):
|
||||
async def bans(self):
|
||||
"""|coro|
|
||||
|
||||
Retrieves all the users that are banned from the guild.
|
||||
|
||||
This coroutine returns a :class:`list` of BanEntry objects, which is a
|
||||
namedtuple with a ``user`` field to denote the :class:`User`
|
||||
that got banned along with a ``reason`` field specifying
|
||||
why the user was banned that could be set to ``None``.
|
||||
Retrieves all the users that are banned from the guild as a :class:`list` of :class:`BanEntry`.
|
||||
|
||||
You must have the :attr:`~Permissions.ban_members` permission
|
||||
to get this information.
|
||||
@ -1453,8 +1505,8 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[BanEntry]
|
||||
A list of BanEntry objects.
|
||||
List[:class:`BanEntry`]
|
||||
A list of :class:`BanEntry` objects.
|
||||
"""
|
||||
|
||||
data = await self._state.http.get_bans(self.id)
|
||||
@ -1513,7 +1565,7 @@ class Guild(Hashable):
|
||||
"""
|
||||
|
||||
if not isinstance(days, int):
|
||||
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
|
||||
raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
|
||||
|
||||
if roles:
|
||||
roles = [str(role.id) for role in roles]
|
||||
@ -1521,6 +1573,29 @@ class Guild(Hashable):
|
||||
data = await self._state.http.prune_members(self.id, days, compute_prune_count=compute_prune_count, roles=roles, reason=reason)
|
||||
return data['pruned']
|
||||
|
||||
async def templates(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the list of templates from this guild.
|
||||
|
||||
Requires :attr:`~.Permissions.manage_guild` permissions.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You don't have permissions to get the templates.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`Template`]
|
||||
The templates for this guild.
|
||||
"""
|
||||
from .template import Template
|
||||
data = await self._state.http.guild_templates(self.id)
|
||||
return [Template(data=d, state=self._state) for d in data]
|
||||
|
||||
async def webhooks(self):
|
||||
"""|coro|
|
||||
|
||||
@ -1543,7 +1618,7 @@ class Guild(Hashable):
|
||||
data = await self._state.http.guild_webhooks(self.id)
|
||||
return [Webhook.from_state(d, state=self._state) for d in data]
|
||||
|
||||
async def estimate_pruned_members(self, *, days):
|
||||
async def estimate_pruned_members(self, *, days, roles=None):
|
||||
"""|coro|
|
||||
|
||||
Similar to :meth:`prune_members` except instead of actually
|
||||
@ -1554,6 +1629,11 @@ class Guild(Hashable):
|
||||
-----------
|
||||
days: :class:`int`
|
||||
The number of days before counting as inactive.
|
||||
roles: Optional[List[:class:`abc.Snowflake`]]
|
||||
A list of :class:`abc.Snowflake` that represent roles to include in the estimate. If a member
|
||||
has a role that is not specified, they'll be excluded.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -1571,9 +1651,12 @@ class Guild(Hashable):
|
||||
"""
|
||||
|
||||
if not isinstance(days, int):
|
||||
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
|
||||
raise InvalidArgument(f'Expected int for ``days``, received {days.__class__.__name__} instead.')
|
||||
|
||||
data = await self._state.http.estimate_pruned_members(self.id, days)
|
||||
if roles:
|
||||
roles = [str(role.id) for role in roles]
|
||||
|
||||
data = await self._state.http.estimate_pruned_members(self.id, days, roles)
|
||||
return data['pruned']
|
||||
|
||||
async def invites(self):
|
||||
@ -1607,6 +1690,36 @@ class Guild(Hashable):
|
||||
|
||||
return result
|
||||
|
||||
async def create_template(self, *, name, description=None):
|
||||
"""|coro|
|
||||
|
||||
Creates a template for the guild.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the template.
|
||||
description: Optional[:class:`str`]
|
||||
The description of the template.
|
||||
"""
|
||||
from .template import Template
|
||||
|
||||
payload = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
if description:
|
||||
payload['description'] = description
|
||||
|
||||
data = await self._state.http.create_template(self.id, payload)
|
||||
|
||||
return Template(state=self._state, data=data)
|
||||
|
||||
async def create_integration(self, *, type, id):
|
||||
"""|coro|
|
||||
|
||||
@ -1786,6 +1899,9 @@ class Guild(Hashable):
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
do this.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
Can now pass ``int`` to ``colour`` keyword-only parameter.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -1838,7 +1954,7 @@ class Guild(Hashable):
|
||||
valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable')
|
||||
for key in fields:
|
||||
if key not in valid_keys:
|
||||
raise InvalidArgument('%r is not a valid field.' % key)
|
||||
raise InvalidArgument(f'{key!r} is not a valid field.')
|
||||
|
||||
data = await self._state.http.create_role(self.id, reason=reason, **fields)
|
||||
role = Role(guild=self, data=data, state=self._state)
|
||||
@ -2031,26 +2147,6 @@ class Guild(Hashable):
|
||||
payload['max_age'] = 0
|
||||
return Invite(state=self._state, data=payload)
|
||||
|
||||
def ack(self):
|
||||
"""|coro|
|
||||
|
||||
Marks every message in this guild as read.
|
||||
|
||||
The user must not be a bot user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Acking failed.
|
||||
ClientException
|
||||
You must not be a bot user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
if state.is_bot:
|
||||
raise ClientException('Must not be a bot account to ack messages.')
|
||||
return state.http.ack_guild(self.id)
|
||||
|
||||
def audit_logs(self, *, limit=100, before=None, after=None, oldest_first=None, user=None, action=None):
|
||||
"""Returns an :class:`AsyncIterator` that enables receiving the guild's audit logs.
|
||||
|
||||
@ -2080,10 +2176,12 @@ class Guild(Hashable):
|
||||
The number of entries to retrieve. If ``None`` retrieve all entries.
|
||||
before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve entries before this date or entry.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
|
||||
Retrieve entries after this date or entry.
|
||||
If a date is provided it must be a timezone-naive datetime representing UTC time.
|
||||
If a datetime is provided, it is recommended to use a UTC aware datetime.
|
||||
If the datetime is naive, it is assumed to be local time.
|
||||
oldest_first: :class:`bool`
|
||||
If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if
|
||||
``after`` is specified, otherwise ``False``.
|
||||
@ -2224,6 +2322,9 @@ class Guild(Hashable):
|
||||
if user_ids is not None and query is not None:
|
||||
raise ValueError('Cannot pass both query and user_ids')
|
||||
|
||||
if user_ids is not None and not user_ids:
|
||||
raise ValueError('user_ids must contain at least 1 value')
|
||||
|
||||
limit = min(100, limit or 5)
|
||||
return await self._state.query_members(self, query=query, limit=limit, user_ids=user_ids, presences=presences, cache=cache)
|
||||
|
||||
|
705
discord/http.py
705
discord/http.py
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -30,6 +28,11 @@ from .user import User
|
||||
from .errors import InvalidArgument
|
||||
from .enums import try_enum, ExpireBehaviour
|
||||
|
||||
__all__ = (
|
||||
'IntegrationAccount',
|
||||
'Integration',
|
||||
)
|
||||
|
||||
class IntegrationAccount:
|
||||
"""Represents an integration account.
|
||||
|
||||
@ -84,7 +87,7 @@ class Integration:
|
||||
account: :class:`IntegrationAccount`
|
||||
The integration account information.
|
||||
synced_at: :class:`datetime.datetime`
|
||||
When the integration was last synced.
|
||||
An aware UTC datetime representing when the integration was last synced.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
|
||||
@ -186,7 +189,7 @@ class Integration:
|
||||
Syncing the integration failed.
|
||||
"""
|
||||
await self._state.http.sync_integration(self.guild.id, self.id)
|
||||
self.synced_at = datetime.datetime.utcnow()
|
||||
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
104
discord/interactions.py
Normal file
104
discord/interactions.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import utils
|
||||
from .enums import try_enum, InteractionType
|
||||
|
||||
__all__ = (
|
||||
'Interaction',
|
||||
)
|
||||
|
||||
class Interaction:
|
||||
"""Represents a Discord interaction.
|
||||
|
||||
An interaction happens when a user does an action that needs to
|
||||
be notified. Current examples are slash commands but future examples
|
||||
include forms and buttons.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The interaction's ID.
|
||||
type: :class:`InteractionType`
|
||||
The interaction type.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID the interaction was sent from.
|
||||
channel_id: Optional[:class:`int`]
|
||||
The channel ID the interaction was sent from.
|
||||
application_id: :class:`int`
|
||||
The application ID that the interaction was for.
|
||||
user: Optional[Union[:class:`User`, :class:`Member`]]
|
||||
The user or member that sent the interaction.
|
||||
token: :class:`str`
|
||||
The token to continue the interaction. These are valid
|
||||
for 15 minutes.
|
||||
"""
|
||||
__slots__ = (
|
||||
'id',
|
||||
'type',
|
||||
'guild_id',
|
||||
'channel_id',
|
||||
'data',
|
||||
'application_id',
|
||||
'user',
|
||||
'token',
|
||||
'version',
|
||||
'_state',
|
||||
)
|
||||
|
||||
def __init__(self, *, data, state=None):
|
||||
self._state = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.id = int(data['id'])
|
||||
self.type = try_enum(InteractionType, data['type'])
|
||||
self.data = data.get('data')
|
||||
self.token = data['token']
|
||||
self.version = data['version']
|
||||
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
|
||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
self.application_id = utils._get_as_snowflake(data, 'application_id')
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
|
||||
return self._state and self._state.get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
|
||||
|
||||
Note that due to a Discord limitation, DM channels are not resolved since there is
|
||||
no data to complete them.
|
||||
"""
|
||||
guild = self.guild
|
||||
return guild and guild.get_channel(self.channel_id)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -30,6 +28,12 @@ from .object import Object
|
||||
from .mixins import Hashable
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
|
||||
__all__ = (
|
||||
'PartialInviteChannel',
|
||||
'PartialInviteGuild',
|
||||
'Invite',
|
||||
)
|
||||
|
||||
class PartialInviteChannel:
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
@ -80,7 +84,7 @@ class PartialInviteChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -258,7 +262,8 @@ class Invite(Hashable):
|
||||
Attributes
|
||||
-----------
|
||||
max_age: :class:`int`
|
||||
How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire.
|
||||
How long the before the invite expires in seconds.
|
||||
A value of ``0`` indicates that it doesn't expire.
|
||||
code: :class:`str`
|
||||
The URL fragment used for the invite.
|
||||
guild: Optional[Union[:class:`Guild`, :class:`Object`, :class:`PartialInviteGuild`]]
|
||||
@ -266,7 +271,7 @@ class Invite(Hashable):
|
||||
revoked: :class:`bool`
|
||||
Indicates if the invite has been revoked.
|
||||
created_at: :class:`datetime.datetime`
|
||||
A datetime object denoting the time the invite was created.
|
||||
An aware UTC datetime object denoting the time the invite was created.
|
||||
temporary: :class:`bool`
|
||||
Indicates that the invite grants temporary membership.
|
||||
If ``True``, members who joined via this invite will be kicked upon disconnect.
|
||||
@ -274,6 +279,7 @@ class Invite(Hashable):
|
||||
How many times the invite has been used.
|
||||
max_uses: :class:`int`
|
||||
How many times the invite can be used.
|
||||
A value of ``0`` indicates that it has unlimited uses.
|
||||
inviter: :class:`User`
|
||||
The user who created the invite.
|
||||
approximate_member_count: Optional[:class:`int`]
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,20 +22,43 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
|
||||
|
||||
from .errors import NoMoreItems
|
||||
from .utils import time_snowflake, maybe_coroutine
|
||||
from .object import Object
|
||||
from .audit_logs import AuditLogEntry
|
||||
|
||||
__all__ = (
|
||||
'ReactionIterator',
|
||||
'HistoryIterator',
|
||||
'AuditLogIterator',
|
||||
'GuildIterator',
|
||||
'MemberIterator',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .member import Member
|
||||
from .user import User
|
||||
from .message import Message
|
||||
from .audit_logs import AuditLogEntry
|
||||
from .guild import Guild
|
||||
|
||||
T = TypeVar('T')
|
||||
OT = TypeVar('OT')
|
||||
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
|
||||
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
|
||||
|
||||
OLDEST_OBJECT = Object(id=0)
|
||||
|
||||
class _AsyncIterator:
|
||||
class _AsyncIterator(AsyncIterator[T]):
|
||||
__slots__ = ()
|
||||
|
||||
def get(self, **attrs):
|
||||
def get(self, **attrs: Any) -> Optional[T]:
|
||||
def predicate(elem):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split('__')
|
||||
@ -51,7 +72,7 @@ class _AsyncIterator:
|
||||
|
||||
return self.find(predicate)
|
||||
|
||||
async def find(self, predicate):
|
||||
async def find(self, predicate: _Predicate[T]) -> Optional[T]:
|
||||
while True:
|
||||
try:
|
||||
elem = await self.next()
|
||||
@ -62,47 +83,35 @@ class _AsyncIterator:
|
||||
if ret:
|
||||
return elem
|
||||
|
||||
def chunk(self, max_size):
|
||||
def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
|
||||
if max_size <= 0:
|
||||
raise ValueError('async iterator chunk sizes must be greater than 0.')
|
||||
return _ChunkedAsyncIterator(self, max_size)
|
||||
|
||||
def map(self, func):
|
||||
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
|
||||
return _MappedAsyncIterator(self, func)
|
||||
|
||||
def filter(self, predicate):
|
||||
def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
|
||||
return _FilteredAsyncIterator(self, predicate)
|
||||
|
||||
async def flatten(self):
|
||||
ret = []
|
||||
while True:
|
||||
try:
|
||||
item = await self.next()
|
||||
except NoMoreItems:
|
||||
return ret
|
||||
else:
|
||||
ret.append(item)
|
||||
async def flatten(self) -> List[T]:
|
||||
return [element async for element in self]
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
async def __anext__(self) -> T:
|
||||
try:
|
||||
msg = await self.next()
|
||||
return await self.next()
|
||||
except NoMoreItems:
|
||||
raise StopAsyncIteration()
|
||||
else:
|
||||
return msg
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
class _ChunkedAsyncIterator(_AsyncIterator):
|
||||
class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, max_size):
|
||||
self.iterator = iterator
|
||||
self.max_size = max_size
|
||||
|
||||
async def next(self):
|
||||
|
||||
async def next(self) -> T:
|
||||
ret = []
|
||||
n = 0
|
||||
while n < self.max_size:
|
||||
@ -117,17 +126,17 @@ class _ChunkedAsyncIterator(_AsyncIterator):
|
||||
n += 1
|
||||
return ret
|
||||
|
||||
class _MappedAsyncIterator(_AsyncIterator):
|
||||
class _MappedAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, func):
|
||||
self.iterator = iterator
|
||||
self.func = func
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
# this raises NoMoreItems and will propagate appropriately
|
||||
item = await self.iterator.next()
|
||||
return await maybe_coroutine(self.func, item)
|
||||
|
||||
class _FilteredAsyncIterator(_AsyncIterator):
|
||||
class _FilteredAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, predicate):
|
||||
self.iterator = iterator
|
||||
|
||||
@ -136,7 +145,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
|
||||
|
||||
self.predicate = predicate
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
getter = self.iterator.next
|
||||
pred = self.predicate
|
||||
while True:
|
||||
@ -146,7 +155,7 @@ class _FilteredAsyncIterator(_AsyncIterator):
|
||||
if ret:
|
||||
return item
|
||||
|
||||
class ReactionIterator(_AsyncIterator):
|
||||
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||
def __init__(self, message, emoji, limit=100, after=None):
|
||||
self.message = message
|
||||
self.limit = limit
|
||||
@ -159,7 +168,7 @@ class ReactionIterator(_AsyncIterator):
|
||||
self.channel_id = message.channel.id
|
||||
self.users = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.users.empty():
|
||||
await self.fill_users()
|
||||
|
||||
@ -194,7 +203,7 @@ class ReactionIterator(_AsyncIterator):
|
||||
else:
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
|
||||
class HistoryIterator(_AsyncIterator):
|
||||
class HistoryIterator(_AsyncIterator['Message']):
|
||||
"""Iterator for receiving a channel's message history.
|
||||
|
||||
The messages endpoint has two behaviours we care about here:
|
||||
@ -280,7 +289,7 @@ class HistoryIterator(_AsyncIterator):
|
||||
if (self.after and self.after != OLDEST_OBJECT):
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.messages.empty():
|
||||
await self.fill_messages()
|
||||
|
||||
@ -291,36 +300,13 @@ class HistoryIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def flatten(self):
|
||||
# this is similar to fill_messages except it uses a list instead
|
||||
# of a queue to place the messages in.
|
||||
result = []
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
while self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
result.append(self.state.create_message(channel=channel, data=element))
|
||||
return result
|
||||
|
||||
async def fill_messages(self):
|
||||
if not hasattr(self, 'channel'):
|
||||
# do the required set up
|
||||
@ -374,7 +360,7 @@ class HistoryIterator(_AsyncIterator):
|
||||
return data
|
||||
return []
|
||||
|
||||
class AuditLogIterator(_AsyncIterator):
|
||||
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
@ -436,7 +422,7 @@ class AuditLogIterator(_AsyncIterator):
|
||||
self.after = Object(id=int(entries[0]['id']))
|
||||
return data.get('users', []), entries
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.entries.empty():
|
||||
await self._fill()
|
||||
|
||||
@ -447,13 +433,10 @@ class AuditLogIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
@ -482,7 +465,7 @@ class AuditLogIterator(_AsyncIterator):
|
||||
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
|
||||
|
||||
|
||||
class GuildIterator(_AsyncIterator):
|
||||
class GuildIterator(_AsyncIterator['Guild']):
|
||||
"""Iterator for receiving the client's guilds.
|
||||
|
||||
The guilds endpoint has the same two behaviours as described
|
||||
@ -536,7 +519,7 @@ class GuildIterator(_AsyncIterator):
|
||||
else:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.guilds.empty():
|
||||
await self.fill_guilds()
|
||||
|
||||
@ -547,13 +530,10 @@ class GuildIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 100:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
@ -561,20 +541,6 @@ class GuildIterator(_AsyncIterator):
|
||||
from .guild import Guild
|
||||
return Guild(state=self.state, data=data)
|
||||
|
||||
async def flatten(self):
|
||||
result = []
|
||||
while self._get_retrieve():
|
||||
data = await self._retrieve_guilds(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0
|
||||
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
result.append(self.create_guild(element))
|
||||
return result
|
||||
|
||||
async def fill_guilds(self):
|
||||
if self._get_retrieve():
|
||||
data = await self._retrieve_guilds(self.retrieve)
|
||||
@ -611,7 +577,7 @@ class GuildIterator(_AsyncIterator):
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
return data
|
||||
|
||||
class MemberIterator(_AsyncIterator):
|
||||
class MemberIterator(_AsyncIterator['Member']):
|
||||
def __init__(self, guild, limit=1000, after=None):
|
||||
|
||||
if isinstance(after, datetime.datetime):
|
||||
@ -625,7 +591,7 @@ class MemberIterator(_AsyncIterator):
|
||||
self.get_members = self.state.http.get_members
|
||||
self.members = asyncio.Queue()
|
||||
|
||||
async def next(self):
|
||||
async def next(self) -> T:
|
||||
if self.members.empty():
|
||||
await self.fill_members()
|
||||
|
||||
@ -636,13 +602,10 @@ class MemberIterator(_AsyncIterator):
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
if l is None or l > 1000:
|
||||
r = 1000
|
||||
elif l <= 1000:
|
||||
r = l
|
||||
else:
|
||||
r = 1000
|
||||
|
||||
r = l
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import itertools
|
||||
import sys
|
||||
from operator import attrgetter
|
||||
@ -31,6 +31,7 @@ from operator import attrgetter
|
||||
import discord.abc
|
||||
|
||||
from . import utils
|
||||
from .errors import ClientException
|
||||
from .user import BaseUser, User
|
||||
from .activity import create_activity
|
||||
from .permissions import Permissions
|
||||
@ -38,6 +39,11 @@ from .enums import Status, try_enum
|
||||
from .colour import Colour
|
||||
from .object import Object
|
||||
|
||||
__all__ = (
|
||||
'VoiceState',
|
||||
'Member',
|
||||
)
|
||||
|
||||
class VoiceState:
|
||||
"""Represents a Discord user's voice state.
|
||||
|
||||
@ -58,15 +64,32 @@ class VoiceState:
|
||||
|
||||
self_video: :class:`bool`
|
||||
Indicates if the user is currently broadcasting video.
|
||||
suppress: :class:`bool`
|
||||
Indicates if the user is suppressed from speaking.
|
||||
|
||||
Only applies to stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
requested_to_speak_at: Optional[:class:`datetime.datetime`]
|
||||
An aware datetime object that specifies the date and time in UTC that the member
|
||||
requested to speak. It will be ``None`` if they are not requesting to speak
|
||||
anymore or have been accepted to speak.
|
||||
|
||||
Only applicable to stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
afk: :class:`bool`
|
||||
Indicates if the user is currently in the AFK channel in the guild.
|
||||
channel: Optional[:class:`VoiceChannel`]
|
||||
channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]]
|
||||
The voice channel that the user is currently connected to. ``None`` if the user
|
||||
is not currently in a voice channel.
|
||||
"""
|
||||
|
||||
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute',
|
||||
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel')
|
||||
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel',
|
||||
'requested_to_speak_at', 'suppress')
|
||||
|
||||
def __init__(self, *, data, channel=None):
|
||||
self.session_id = data.get('session_id')
|
||||
@ -80,10 +103,21 @@ class VoiceState:
|
||||
self.afk = data.get('suppress', False)
|
||||
self.mute = data.get('mute', False)
|
||||
self.deaf = data.get('deaf', False)
|
||||
self.suppress = data.get('suppress', False)
|
||||
self.requested_to_speak_at = utils.parse_time(data.get('request_to_speak_timestamp'))
|
||||
self.channel = channel
|
||||
|
||||
def __repr__(self):
|
||||
return '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} self_stream={0.self_stream} channel={0.channel!r}>'.format(self)
|
||||
attrs = [
|
||||
('self_mute', self.self_mute),
|
||||
('self_deaf', self.self_deaf),
|
||||
('self_stream', self.self_stream),
|
||||
('suppress', self.suppress),
|
||||
('requested_to_speak_at', self.requested_to_speak_at),
|
||||
('channel', self.channel)
|
||||
]
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {inner}>'
|
||||
|
||||
def flatten_user(cls):
|
||||
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
|
||||
@ -99,21 +133,26 @@ def flatten_user(cls):
|
||||
# slotted members are implemented as member_descriptors in Type.__dict__
|
||||
if not hasattr(value, '__annotations__'):
|
||||
getter = attrgetter('_user.' + attr)
|
||||
setattr(cls, attr, property(getter, doc='Equivalent to :attr:`User.%s`' % attr))
|
||||
setattr(cls, attr, property(getter, doc=f'Equivalent to :attr:`User.{attr}`'))
|
||||
else:
|
||||
# Technically, this can also use attrgetter
|
||||
# However I'm not sure how I feel about "functions" returning properties
|
||||
# It probably breaks something in Sphinx.
|
||||
# probably a member function by now
|
||||
def generate_function(x):
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
# We want sphinx to properly show coroutine functions as coroutines
|
||||
if inspect.iscoroutinefunction(value):
|
||||
async def general(self, *args, **kwargs):
|
||||
return await getattr(self._user, x)(*args, **kwargs)
|
||||
else:
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
|
||||
general.__name__ = x
|
||||
return general
|
||||
|
||||
func = generate_function(attr)
|
||||
func.__doc__ = value.__doc__
|
||||
func = utils.copy_doc(value)(func)
|
||||
setattr(cls, attr, func)
|
||||
|
||||
return cls
|
||||
@ -149,7 +188,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
Attributes
|
||||
----------
|
||||
joined_at: Optional[:class:`datetime.datetime`]
|
||||
A datetime object that specifies the date and time in UTC that the member joined the guild.
|
||||
An aware datetime object that specifies the date and time in UTC that the member joined the guild.
|
||||
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
|
||||
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
|
||||
The activities that the user is currently doing.
|
||||
@ -162,7 +201,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
.. versionadded:: 1.6
|
||||
premium_since: Optional[:class:`datetime.datetime`]
|
||||
A datetime object that specifies the date and time in UTC when the member used their
|
||||
An aware datetime object that specifies the date and time in UTC when the member used their
|
||||
Nitro boost on the guild, if available. This could be ``None``.
|
||||
"""
|
||||
|
||||
@ -190,8 +229,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}' \
|
||||
' bot={1.bot} nick={0.nick!r} guild={0.guild!r}>'.format(self, self._user)
|
||||
return f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}' \
|
||||
f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _BaseUser) and other.id == self.id
|
||||
@ -293,12 +332,12 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
def _update_inner_user(self, user):
|
||||
u = self._user
|
||||
original = (u.name, u.avatar, u.discriminator)
|
||||
original = (u.name, u.avatar, u.discriminator, u._public_flags)
|
||||
# These keys seem to always be available
|
||||
modified = (user['username'], user['avatar'], user['discriminator'])
|
||||
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
|
||||
if original != modified:
|
||||
to_return = User._copy(self._user)
|
||||
u.name, u.avatar, u.discriminator = modified
|
||||
u.name, u.avatar, u.discriminator, u._public_flags = modified
|
||||
# Signal to dispatch on_user_update
|
||||
return to_return, u
|
||||
|
||||
@ -390,8 +429,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention the member."""
|
||||
if self.nick:
|
||||
return '<@!%s>' % self.id
|
||||
return '<@%s>' % self.id
|
||||
return f'<@!{self._user.id}>'
|
||||
return f'<@{self._user.id}>'
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@ -401,7 +440,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if they have a guild specific nickname then that
|
||||
is returned instead.
|
||||
"""
|
||||
return self.nick if self.nick is not None else self.name
|
||||
return self.nick or self.name
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
@ -434,11 +473,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if self._user.mentioned_in(message):
|
||||
return True
|
||||
|
||||
for role in message.role_mentions:
|
||||
if self._roles.has(role.id):
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(self._roles.has(role.id) for role in message.role_mentions)
|
||||
|
||||
def permissions_in(self, channel):
|
||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
||||
@ -560,6 +595,11 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
Indicates if the member should be guild muted or un-muted.
|
||||
deafen: :class:`bool`
|
||||
Indicates if the member should be guild deafened or un-deafened.
|
||||
suppress: :class:`bool`
|
||||
Indicates if the member should be suppressed in stage channels.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
roles: Optional[List[:class:`Role`]]
|
||||
The member's new list of roles. This *replaces* the roles.
|
||||
voice_channel: Optional[:class:`VoiceChannel`]
|
||||
@ -577,6 +617,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
"""
|
||||
http = self._state.http
|
||||
guild_id = self.guild.id
|
||||
me = self._state.self_id == self.id
|
||||
payload = {}
|
||||
|
||||
try:
|
||||
@ -585,8 +626,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
# nick not present so...
|
||||
pass
|
||||
else:
|
||||
nick = nick if nick else ''
|
||||
if self._state.self_id == self.id:
|
||||
nick = nick or ''
|
||||
if me:
|
||||
await http.change_my_nickname(guild_id, nick, reason=reason)
|
||||
else:
|
||||
payload['nick'] = nick
|
||||
@ -599,6 +640,23 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if mute is not None:
|
||||
payload['mute'] = mute
|
||||
|
||||
suppress = fields.get('suppress')
|
||||
if suppress is not None:
|
||||
voice_state_payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'suppress': suppress,
|
||||
}
|
||||
|
||||
if suppress or self.bot:
|
||||
voice_state_payload['request_to_speak_timestamp'] = None
|
||||
|
||||
if me:
|
||||
await http.edit_my_voice_state(guild_id, voice_state_payload)
|
||||
else:
|
||||
if not suppress:
|
||||
voice_state_payload['request_to_speak_timestamp'] = datetime.datetime.utcnow().isoformat()
|
||||
await http.edit_voice_state(guild_id, self.id, voice_state_payload)
|
||||
|
||||
try:
|
||||
vc = fields['voice_channel']
|
||||
except KeyError:
|
||||
@ -613,10 +671,43 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
else:
|
||||
payload['roles'] = tuple(r.id for r in roles)
|
||||
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
if payload:
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
|
||||
# TODO: wait for WS event for modify-in-place behaviour
|
||||
|
||||
async def request_to_speak(self):
|
||||
"""|coro|
|
||||
|
||||
Request to speak in the connected channel.
|
||||
|
||||
Only applies to stage channels.
|
||||
|
||||
.. note::
|
||||
|
||||
Requesting members that are not the client is equivalent
|
||||
to :attr:`.edit` providing ``suppress`` as ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have the proper permissions to the action requested.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
"""
|
||||
payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'request_to_speak_timestamp': datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
if self._state.self_id != self.id:
|
||||
payload['suppress'] = False
|
||||
await self._state.http.edit_voice_state(self.guild.id, self.id, payload)
|
||||
else:
|
||||
await self._state.http.edit_my_voice_state(self.guild.id, payload)
|
||||
|
||||
async def move_to(self, channel, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'AllowedMentions',
|
||||
)
|
||||
|
||||
class _FakeBool:
|
||||
def __repr__(self):
|
||||
return 'True'
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -33,7 +31,6 @@ from . import utils
|
||||
from .reaction import Reaction
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .calls import CallMessage
|
||||
from .enums import MessageType, ChannelType, try_enum
|
||||
from .errors import InvalidArgument, ClientException, HTTPException
|
||||
from .embeds import Embed
|
||||
@ -58,7 +55,7 @@ def convert_emoji_reaction(emoji):
|
||||
emoji = emoji.emoji
|
||||
|
||||
if isinstance(emoji, Emoji):
|
||||
return '%s:%s' % (emoji.name, emoji.id)
|
||||
return f'{emoji.name}:{emoji.id}'
|
||||
if isinstance(emoji, PartialEmoji):
|
||||
return emoji._as_reaction()
|
||||
if isinstance(emoji, str):
|
||||
@ -66,11 +63,32 @@ def convert_emoji_reaction(emoji):
|
||||
# No existing emojis have <> in them, so this should be okay.
|
||||
return emoji.strip('<>')
|
||||
|
||||
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
|
||||
raise InvalidArgument(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.')
|
||||
|
||||
class Attachment:
|
||||
class Attachment(Hashable):
|
||||
"""Represents an attachment from Discord.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the URL of the attachment.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the attachment is equal to another attachment.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the attachment is not equal to another attachment.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the attachment.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Attachment can now be casted to :class:`str` and is hashable.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
id: :class:`int`
|
||||
@ -90,9 +108,13 @@ class Attachment:
|
||||
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
|
||||
case of images. When the message is deleted, this URL might be valid for a few
|
||||
minutes or not valid at all.
|
||||
content_type: Optional[:class:`str`]
|
||||
The attachment's `media type <https://en.wikipedia.org/wiki/Media_type>`_
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http')
|
||||
__slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http', 'content_type')
|
||||
|
||||
def __init__(self, *, data, state):
|
||||
self.id = int(data['id'])
|
||||
@ -103,6 +125,7 @@ class Attachment:
|
||||
self.url = data.get('url')
|
||||
self.proxy_url = data.get('proxy_url')
|
||||
self._http = state.http
|
||||
self.content_type = data.get('content_type')
|
||||
|
||||
def is_spoiler(self):
|
||||
""":class:`bool`: Whether this attachment contains a spoiler."""
|
||||
@ -114,6 +137,9 @@ class Attachment:
|
||||
def __repr__(self):
|
||||
return '<Attachment id={0.id} filename={0.filename!r} url={0.url!r}>'.format(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.url or ''
|
||||
|
||||
async def save(self, fp, *, seek_begin=True, use_cached=False):
|
||||
"""|coro|
|
||||
|
||||
@ -282,6 +308,12 @@ class MessageReference:
|
||||
The channel id of the message referenced.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild id of the message referenced.
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying to the referenced message should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
resolved: Optional[Union[:class:`Message`, :class:`DeletedReferencedMessage`]]
|
||||
The message that this reference resolved to. If this is ``None``
|
||||
then the original message was not fetched either due to the Discord API
|
||||
@ -294,14 +326,15 @@ class MessageReference:
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'resolved', '_state')
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state')
|
||||
|
||||
def __init__(self, *, message_id, channel_id, guild_id=None):
|
||||
def __init__(self, *, message_id, channel_id, guild_id=None, fail_if_not_exists=True):
|
||||
self._state = None
|
||||
self.resolved = None
|
||||
self.message_id = message_id
|
||||
self.channel_id = channel_id
|
||||
self.guild_id = guild_id
|
||||
self.fail_if_not_exists = fail_if_not_exists
|
||||
|
||||
@classmethod
|
||||
def with_state(cls, state, data):
|
||||
@ -314,7 +347,7 @@ class MessageReference:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message):
|
||||
def from_message(cls, message, *, fail_if_not_exists=True):
|
||||
"""Creates a :class:`MessageReference` from an existing :class:`~discord.Message`.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
@ -323,13 +356,18 @@ class MessageReference:
|
||||
----------
|
||||
message: :class:`~discord.Message`
|
||||
The message to be converted into a reference.
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying to the referenced message should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`MessageReference`
|
||||
A reference to the message.
|
||||
"""
|
||||
self = cls(message_id=message.id, channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None))
|
||||
self = cls(message_id=message.id, channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None), fail_if_not_exists=fail_if_not_exists)
|
||||
self._state = message._state
|
||||
return self
|
||||
|
||||
@ -356,6 +394,15 @@ class MessageReference:
|
||||
"""Optional[:class:`~discord.Message`]: The cached message, if found in the internal message cache."""
|
||||
return self._state._get_message(self.message_id)
|
||||
|
||||
@property
|
||||
def jump_url(self):
|
||||
""":class:`str`: Returns a URL that allows the client to jump to the referenced message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
guild_id = self.guild_id if self.guild_id is not None else '@me'
|
||||
return 'https://discord.com/channels/{0}/{1.channel_id}/{1.message_id}'.format(guild_id, self)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MessageReference message_id={0.message_id!r} channel_id={0.channel_id!r} guild_id={0.guild_id!r}>'.format(self)
|
||||
|
||||
@ -364,6 +411,8 @@ class MessageReference:
|
||||
result['channel_id'] = self.channel_id
|
||||
if self.guild_id is not None:
|
||||
result['guild_id'] = self.guild_id
|
||||
if self.fail_if_not_exists is not None:
|
||||
result['fail_if_not_exists'] = self.fail_if_not_exists
|
||||
return result
|
||||
|
||||
to_message_reference_dict = to_dict
|
||||
@ -418,15 +467,12 @@ class Message(Hashable):
|
||||
The actual contents of the message.
|
||||
nonce
|
||||
The value used by the discord guild and the client to verify that the message is successfully sent.
|
||||
This is typically non-important.
|
||||
This is not stored long term within Discord's servers and is only used ephemerally.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds the message has.
|
||||
channel: Union[:class:`abc.Messageable`]
|
||||
The :class:`TextChannel` that the message was sent from.
|
||||
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
|
||||
call: Optional[:class:`CallMessage`]
|
||||
The call that the message refers to. This is only applicable to messages of type
|
||||
:attr:`MessageType.call`.
|
||||
reference: Optional[:class:`~discord.MessageReference`]
|
||||
The message that this message references. This is only applicable to messages of
|
||||
type :attr:`MessageType.pins_add`, crossposted messages created by a
|
||||
@ -502,7 +548,7 @@ class Message(Hashable):
|
||||
'mention_everyone', 'embeds', 'id', 'mentions', 'author',
|
||||
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
|
||||
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
|
||||
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags',
|
||||
'role_mentions', '_cs_raw_role_mentions', 'type', 'flags',
|
||||
'_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference',
|
||||
'application', 'activity', 'stickers')
|
||||
|
||||
@ -548,9 +594,9 @@ class Message(Hashable):
|
||||
|
||||
ref.resolved = self.__class__(channel=chan, data=resolved, state=state)
|
||||
|
||||
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
|
||||
for handler in ('author', 'member', 'mentions', 'mention_roles', 'flags'):
|
||||
try:
|
||||
getattr(self, '_handle_%s' % handler)(data[handler])
|
||||
getattr(self, f'_handle_{handler}')(data[handler])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
@ -722,26 +768,6 @@ class Message(Hashable):
|
||||
if role is not None:
|
||||
self.role_mentions.append(role)
|
||||
|
||||
def _handle_call(self, call):
|
||||
if call is None or self.type is not MessageType.call:
|
||||
self.call = None
|
||||
return
|
||||
|
||||
# we get the participant source from the mentions array or
|
||||
# the author
|
||||
|
||||
participants = []
|
||||
for uid in map(int, call.get('participants', [])):
|
||||
if uid == self.author.id:
|
||||
participants.append(self.author)
|
||||
else:
|
||||
user = utils.find(lambda u: u.id == uid, self.mentions)
|
||||
if user is not None:
|
||||
participants.append(user)
|
||||
|
||||
call['participants'] = participants
|
||||
self.call = CallMessage(message=self, **call)
|
||||
|
||||
def _rebind_channel_reference(self, new_channel):
|
||||
self.channel = new_channel
|
||||
|
||||
@ -798,24 +824,24 @@ class Message(Hashable):
|
||||
|
||||
.. note::
|
||||
|
||||
This *does not* escape markdown. If you want to escape
|
||||
markdown then use :func:`utils.escape_markdown` along
|
||||
with this function.
|
||||
This *does not* affect markdown. If you want to escape
|
||||
or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown`
|
||||
respectively, along with this function.
|
||||
"""
|
||||
|
||||
transformations = {
|
||||
re.escape('<#%s>' % channel.id): '#' + channel.name
|
||||
re.escape(f'<#{channel.id}>'): '#' + channel.name
|
||||
for channel in self.channel_mentions
|
||||
}
|
||||
|
||||
mention_transforms = {
|
||||
re.escape('<@%s>' % member.id): '@' + member.display_name
|
||||
re.escape(f'<@{member.id}>'): '@' + member.display_name
|
||||
for member in self.mentions
|
||||
}
|
||||
|
||||
# add the <@!user_id> cases as well..
|
||||
second_mention_transforms = {
|
||||
re.escape('<@!%s>' % member.id): '@' + member.display_name
|
||||
re.escape(f'<@!{member.id}>'): '@' + member.display_name
|
||||
for member in self.mentions
|
||||
}
|
||||
|
||||
@ -824,7 +850,7 @@ class Message(Hashable):
|
||||
|
||||
if self.guild is not None:
|
||||
role_transforms = {
|
||||
re.escape('<@&%s>' % role.id): '@' + role.name
|
||||
re.escape(f'<@&{role.id}>'): '@' + role.name
|
||||
for role in self.role_mentions
|
||||
}
|
||||
transformations.update(role_transforms)
|
||||
@ -843,7 +869,7 @@ class Message(Hashable):
|
||||
|
||||
@property
|
||||
def edited_at(self):
|
||||
"""Optional[:class:`datetime.datetime`]: A naive UTC datetime object containing the edited time of the message."""
|
||||
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message."""
|
||||
return self._edited_timestamp
|
||||
|
||||
@property
|
||||
@ -873,7 +899,7 @@ class Message(Hashable):
|
||||
return self.content
|
||||
|
||||
if self.type is MessageType.pins_add:
|
||||
return '{0.name} pinned a message to this channel.'.format(self.author)
|
||||
return f'{self.author.name} pinned a message to this channel.'
|
||||
|
||||
if self.type is MessageType.recipient_add:
|
||||
return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0])
|
||||
@ -885,7 +911,7 @@ class Message(Hashable):
|
||||
return '{0.author.name} changed the channel name: {0.content}'.format(self)
|
||||
|
||||
if self.type is MessageType.channel_icon_change:
|
||||
return '{0.author.name} changed the channel icon.'.format(self)
|
||||
return f'{self.author.name} changed the channel icon.'
|
||||
|
||||
if self.type is MessageType.new_member:
|
||||
formats = [
|
||||
@ -904,27 +930,11 @@ class Message(Hashable):
|
||||
"Yay you made it, {0}!",
|
||||
]
|
||||
|
||||
# manually reconstruct the epoch with millisecond precision, because
|
||||
# datetime.datetime.timestamp() doesn't return the exact posix
|
||||
# timestamp with the precision that we need
|
||||
created_at_ms = int((self.created_at - datetime.datetime(1970, 1, 1)).total_seconds() * 1000)
|
||||
created_at_ms = int(self.created_at.timestamp() * 1000)
|
||||
return formats[created_at_ms % len(formats)].format(self.author.name)
|
||||
|
||||
if self.type is MessageType.call:
|
||||
# we're at the call message type now, which is a bit more complicated.
|
||||
# we can make the assumption that Message.channel is a PrivateChannel
|
||||
# with the type ChannelType.group or ChannelType.private
|
||||
call_ended = self.call.ended_timestamp is not None
|
||||
|
||||
if self.channel.me in self.call.participants:
|
||||
return '{0.author.name} started a call.'.format(self)
|
||||
elif call_ended:
|
||||
return 'You missed a call from {0.author.name}'.format(self)
|
||||
else:
|
||||
return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self)
|
||||
|
||||
if self.type is MessageType.premium_guild_subscription:
|
||||
return '{0.author.name} just boosted the server!'.format(self)
|
||||
return f'{self.author.name} just boosted the server!'
|
||||
|
||||
if self.type is MessageType.premium_guild_tier_1:
|
||||
return '{0.author.name} just boosted the server! {0.guild} has achieved **Level 1!**'.format(self)
|
||||
@ -938,7 +948,22 @@ class Message(Hashable):
|
||||
if self.type is MessageType.channel_follow_add:
|
||||
return '{0.author.name} has added {0.content} to this channel'.format(self)
|
||||
|
||||
async def delete(self, *, delay=None, silent=False):
|
||||
if self.type is MessageType.guild_stream:
|
||||
return '{0.author.name} is live! Now streaming {0.author.activity.name}'.format(self)
|
||||
|
||||
if self.type is MessageType.guild_discovery_disqualified:
|
||||
return 'This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'
|
||||
|
||||
if self.type is MessageType.guild_discovery_requalified:
|
||||
return 'This server is eligible for Server Discovery again and has been automatically relisted!'
|
||||
|
||||
if self.type is MessageType.guild_discovery_grace_period_initial_warning:
|
||||
return 'This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'
|
||||
|
||||
if self.type is MessageType.guild_discovery_grace_period_final_warning:
|
||||
return 'This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'
|
||||
|
||||
async def delete(self, *, delay=None):
|
||||
"""|coro|
|
||||
|
||||
Deletes the message.
|
||||
@ -978,7 +1003,7 @@ class Message(Hashable):
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(delete(), loop=self._state.loop)
|
||||
asyncio.create_task(delete())
|
||||
else:
|
||||
try:
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
@ -1272,26 +1297,6 @@ class Message(Hashable):
|
||||
"""
|
||||
await self._state.http.clear_reactions(self.channel.id, self.id)
|
||||
|
||||
async def ack(self):
|
||||
"""|coro|
|
||||
|
||||
Marks this message as read.
|
||||
|
||||
The user must not be a bot user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Acking failed.
|
||||
ClientException
|
||||
You must not be a bot user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
if state.is_bot:
|
||||
raise ClientException('Must not be a bot account to ack messages.')
|
||||
return await state.http.ack_message(self.channel.id, self.id)
|
||||
|
||||
async def reply(self, content=None, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
@ -1318,18 +1323,26 @@ class Message(Hashable):
|
||||
|
||||
return await self.channel.send(content, reference=self, **kwargs)
|
||||
|
||||
def to_reference(self):
|
||||
def to_reference(self, *, fail_if_not_exists=True):
|
||||
"""Creates a :class:`~discord.MessageReference` from the current message.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fail_if_not_exists: :class:`bool`
|
||||
Whether replying using the message reference should raise :class:`HTTPException`
|
||||
if the message no longer exists or Discord could not fetch the message.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`~discord.MessageReference`
|
||||
The reference to this message.
|
||||
"""
|
||||
|
||||
return MessageReference.from_message(self)
|
||||
return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists)
|
||||
|
||||
def to_message_reference_dict(self):
|
||||
data = {
|
||||
@ -1389,7 +1402,6 @@ class PartialMessage(Hashable):
|
||||
_exported_names = (
|
||||
'jump_url',
|
||||
'delete',
|
||||
'edit',
|
||||
'publish',
|
||||
'pin',
|
||||
'unpin',
|
||||
@ -1404,7 +1416,7 @@ class PartialMessage(Hashable):
|
||||
|
||||
def __init__(self, *, channel, id):
|
||||
if channel.type not in (ChannelType.text, ChannelType.news, ChannelType.private):
|
||||
raise TypeError('Expected TextChannel or DMChannel not %r' % type(channel))
|
||||
raise TypeError(f'Expected TextChannel or DMChannel not {type(channel)!r}')
|
||||
|
||||
self.channel = channel
|
||||
self._state = channel._state
|
||||
@ -1454,3 +1466,102 @@ class PartialMessage(Hashable):
|
||||
|
||||
data = await self._state.http.get_message(self.channel.id, self.id)
|
||||
return self._state.create_message(channel=self.channel, data=data)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the message.
|
||||
|
||||
The content must be able to be transformed into a string via ``str(content)``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
:class:`discord.Message` is returned instead of ``None`` if an edit took place.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: Optional[:class:`str`]
|
||||
The new content to replace the message with.
|
||||
Could be ``None`` to remove the content.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The new embed to replace the original with.
|
||||
Could be ``None`` to remove the embed.
|
||||
suppress: :class:`bool`
|
||||
Whether to suppress embeds for the message. This removes
|
||||
all the embeds if set to ``True``. If set to ``False``
|
||||
this brings the embeds back if they were suppressed.
|
||||
Using this parameter requires :attr:`~.Permissions.manage_messages`.
|
||||
delete_after: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait in the background
|
||||
before deleting the message we just edited. If the deletion fails,
|
||||
then it is silently ignored.
|
||||
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
|
||||
Controls the mentions being processed in this message. If this is
|
||||
passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
|
||||
The merging behaviour only overrides attributes that have been explicitly passed
|
||||
to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
|
||||
If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
|
||||
are used instead.
|
||||
|
||||
Raises
|
||||
-------
|
||||
NotFound
|
||||
The message was not found.
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
Forbidden
|
||||
Tried to suppress a message without permissions or
|
||||
edited a message's content or embed that isn't yours.
|
||||
|
||||
Returns
|
||||
---------
|
||||
Optional[:class:`Message`]
|
||||
The message that was edited.
|
||||
"""
|
||||
|
||||
try:
|
||||
content = fields['content']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if content is not None:
|
||||
fields['content'] = str(content)
|
||||
|
||||
try:
|
||||
embed = fields['embed']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if embed is not None:
|
||||
fields['embed'] = embed.to_dict()
|
||||
|
||||
try:
|
||||
suppress = fields.pop('suppress')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
flags = MessageFlags._from_value(0)
|
||||
flags.suppress_embeds = suppress
|
||||
fields['flags'] = flags.value
|
||||
|
||||
delete_after = fields.pop('delete_after', None)
|
||||
|
||||
try:
|
||||
allowed_mentions = fields.pop('allowed_mentions')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if allowed_mentions is not None:
|
||||
if self._state.allowed_mentions is not None:
|
||||
allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions).to_dict()
|
||||
else:
|
||||
allowed_mentions = allowed_mentions.to_dict()
|
||||
fields['allowed_mentions'] = allowed_mentions
|
||||
|
||||
if fields:
|
||||
data = await self._state.http.edit_message(self.channel.id, self.id, **fields)
|
||||
|
||||
if delete_after is not None:
|
||||
await self.delete(delay=delete_after)
|
||||
|
||||
if fields:
|
||||
return self._state.create_message(channel=self.channel, data=data)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,6 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'EqualityComparable',
|
||||
'Hashable',
|
||||
)
|
||||
|
||||
class EqualityComparable:
|
||||
__slots__ = ()
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,6 +25,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
from . import utils
|
||||
from .mixins import Hashable
|
||||
|
||||
__all__ = (
|
||||
'Object',
|
||||
)
|
||||
|
||||
class Object(Hashable):
|
||||
"""Represents a generic Discord object.
|
||||
|
||||
@ -65,12 +67,12 @@ class Object(Hashable):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
raise TypeError('id parameter must be convertable to int not {0.__class__!r}'.format(id)) from None
|
||||
raise TypeError(f'id parameter must be convertable to int not {id.__class__!r}') from None
|
||||
else:
|
||||
self.id = id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Object id=%r>' % self.id
|
||||
return f'<Object id={self.id!r}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -28,6 +26,12 @@ import struct
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'OggError',
|
||||
'OggPage',
|
||||
'OggStream',
|
||||
)
|
||||
|
||||
class OggError(DiscordException):
|
||||
"""An exception that is thrown for Ogg stream parsing errors."""
|
||||
pass
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -35,6 +33,12 @@ import sys
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'Encoder',
|
||||
'OpusError',
|
||||
'OpusNotLoaded',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
@ -185,7 +189,7 @@ def _load_default():
|
||||
_basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
_bitness = struct.calcsize('P') * 8
|
||||
_target = 'x64' if _bitness > 32 else 'x86'
|
||||
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target))
|
||||
_filename = os.path.join(_basedir, 'bin', f'libopus-0.{_target}.dll')
|
||||
_lib = libopus_loader(_filename)
|
||||
else:
|
||||
_lib = libopus_loader(ctypes.util.find_library('opus'))
|
||||
@ -276,17 +280,14 @@ class _OpusStruct:
|
||||
|
||||
@staticmethod
|
||||
def get_opus_version() -> str:
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
if not is_loaded() and not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
return _lib.opus_get_version_string().decode('utf-8')
|
||||
|
||||
class Encoder(_OpusStruct):
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self.application = application
|
||||
self._state = self._create_state()
|
||||
@ -313,14 +314,14 @@ class Encoder(_OpusStruct):
|
||||
|
||||
def set_bandwidth(self, req):
|
||||
if req not in band_ctl:
|
||||
raise KeyError('%r is not a valid bandwidth setting. Try one of: %s' % (req, ','.join(band_ctl)))
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}')
|
||||
|
||||
k = band_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
|
||||
|
||||
def set_signal_type(self, req):
|
||||
if req not in signal_ctl:
|
||||
raise KeyError('%r is not a valid signal setting. Try one of: %s' % (req, ','.join(signal_ctl)))
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
|
||||
|
||||
k = signal_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
|
||||
@ -342,9 +343,7 @@ class Encoder(_OpusStruct):
|
||||
|
||||
class Decoder(_OpusStruct):
|
||||
def __init__(self):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self._state = self._create_state()
|
||||
|
||||
@ -374,7 +373,7 @@ class Decoder(_OpusStruct):
|
||||
|
||||
def _set_gain(self, adjustment):
|
||||
"""Configures decoder gain adjustment.
|
||||
|
||||
|
||||
Scales the decoded output by a factor specified in Q8 dB units.
|
||||
This has a maximum range of -32768 to 32767 inclusive, and returns
|
||||
OPUS_BAD_ARG (-1) otherwise. The default is zero indicating no adjustment.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,6 +25,9 @@ DEALINGS IN THE SOFTWARE.
|
||||
from .asset import Asset
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'PartialEmoji',
|
||||
)
|
||||
|
||||
class _EmojiTag:
|
||||
__slots__ = ()
|
||||
@ -103,8 +104,8 @@ class PartialEmoji(_EmojiTag):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
if self.animated:
|
||||
return '<a:%s:%s>' % (self.name, self.id)
|
||||
return '<:%s:%s>' % (self.name, self.id)
|
||||
return f'<a:{self.name}:{self.id}>'
|
||||
return f'<:{self.name}:{self.id}>'
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
|
||||
@ -134,7 +135,7 @@ class PartialEmoji(_EmojiTag):
|
||||
def _as_reaction(self):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
return '%s:%s' % (self.name, self.id)
|
||||
return f'{self.name}:{self.id}'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -149,10 +150,43 @@ class PartialEmoji(_EmojiTag):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`Asset`: Returns an asset of the emoji, if it is custom."""
|
||||
""":class:`Asset`: Returns the asset of the emoji, if it is custom.
|
||||
|
||||
This is equivalent to calling :meth:`url_as` with
|
||||
the default parameters (i.e. png/gif detection).
|
||||
"""
|
||||
return self.url_as(format=None)
|
||||
|
||||
def url_as(self, *, format=None, static_format="png"):
|
||||
"""Returns an :class:`Asset` for the emoji's url, if it is custom.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
||||
'gif' is only valid for animated emojis.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the emojis to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected as either 'gif' or static_format, depending on whether the
|
||||
emoji is animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated emoji's to.
|
||||
Defaults to 'png'
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or ``static_format``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if self.is_unicode_emoji():
|
||||
return Asset(self._state)
|
||||
|
||||
_format = 'gif' if self.animated else 'png'
|
||||
url = "/emojis/{0.id}.{1}".format(self, _format)
|
||||
return Asset(self._state, url)
|
||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -96,12 +94,12 @@ class Permissions(BaseFlags):
|
||||
|
||||
def __init__(self, permissions=0, **kwargs):
|
||||
if not isinstance(permissions, int):
|
||||
raise TypeError('Expected int parameter, received %s instead.' % permissions.__class__.__name__)
|
||||
raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
|
||||
|
||||
self.value = permissions
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid permission name.' % key)
|
||||
raise TypeError(f'{key!r} is not a valid permission name.')
|
||||
setattr(self, key, value)
|
||||
|
||||
def is_subset(self, other):
|
||||
@ -109,14 +107,14 @@ class Permissions(BaseFlags):
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value & other.value) == self.value
|
||||
else:
|
||||
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__))
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_superset(self, other):
|
||||
"""Returns ``True`` if self has the same or more permissions as other."""
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value | other.value) == self.value
|
||||
else:
|
||||
raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__))
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_strict_subset(self, other):
|
||||
"""Returns ``True`` if the permissions on other are a strict subset of those on self."""
|
||||
@ -140,8 +138,9 @@ class Permissions(BaseFlags):
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to ``True``."""
|
||||
return cls(0b01111111111111111111111111111111)
|
||||
permissions set to ``True``.
|
||||
"""
|
||||
return cls(0b111111111111111111111111111111111)
|
||||
|
||||
@classmethod
|
||||
def all_channel(cls):
|
||||
@ -149,26 +148,53 @@ class Permissions(BaseFlags):
|
||||
``True`` and the guild-specific ones set to ``False``. The guild-specific
|
||||
permissions are currently:
|
||||
|
||||
- manage_guild
|
||||
- kick_members
|
||||
- ban_members
|
||||
- administrator
|
||||
- change_nickname
|
||||
- manage_nicknames
|
||||
- :attr:`manage_emojis`
|
||||
- :attr:`view_audit_log`
|
||||
- :attr:`view_guild_insights`
|
||||
- :attr:`manage_guild`
|
||||
- :attr:`change_nickname`
|
||||
- :attr:`manage_nicknames`
|
||||
- :attr:`kick_members`
|
||||
- :attr:`ban_members`
|
||||
- :attr:`administrator`
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_slash_commands` permissions.
|
||||
"""
|
||||
return cls(0b00110011111101111111110001010001)
|
||||
return cls(0b10110011111101111111111101010001)
|
||||
|
||||
@classmethod
|
||||
def general(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"General" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b01111100000010000000000010111111)
|
||||
"General" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Permission :attr:`read_messages` is now included in the general permissions, but
|
||||
permissions :attr:`administrator`, :attr:`create_instant_invite`, :attr:`kick_members`,
|
||||
:attr:`ban_members`, :attr:`change_nickname` and :attr:`manage_nicknames` are
|
||||
no longer part of the general permissions.
|
||||
"""
|
||||
return cls(0b01110000000010000000010010110000)
|
||||
|
||||
@classmethod
|
||||
def membership(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Membership" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(0b00001100000000000000000000000111)
|
||||
|
||||
@classmethod
|
||||
def text(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Text" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b00000000000001111111110001000000)
|
||||
"Text" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Permission :attr:`read_messages` is no longer part of the text permissions.
|
||||
Added :attr:`use_slash_commands` permission.
|
||||
"""
|
||||
return cls(0b10000000000001111111100001000000)
|
||||
|
||||
@classmethod
|
||||
def voice(cls):
|
||||
@ -176,6 +202,32 @@ class Permissions(BaseFlags):
|
||||
"Voice" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b00000011111100000000001100000000)
|
||||
|
||||
@classmethod
|
||||
def stage(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Channel" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(1 << 32)
|
||||
|
||||
@classmethod
|
||||
def stage_moderator(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Moderator" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(0b100000001010000000000000000000000)
|
||||
|
||||
@classmethod
|
||||
def advanced(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Advanced" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return cls(1 << 3)
|
||||
|
||||
def update(self, **kwargs):
|
||||
r"""Bulk updates this permission object.
|
||||
@ -405,9 +457,21 @@ class Permissions(BaseFlags):
|
||||
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis."""
|
||||
return 1 << 30
|
||||
|
||||
# 1 unused
|
||||
@flag_value
|
||||
def use_slash_commands(self):
|
||||
""":class:`bool`: Returns ``True`` if a user can use slash commands.
|
||||
|
||||
# after these 32 bits, there's 21 more unused ones technically
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return 1 << 31
|
||||
|
||||
@flag_value
|
||||
def request_to_speak(self):
|
||||
""":class:`bool`: Returns ``True`` if a user can request to speak in a stage channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return 1 << 32
|
||||
|
||||
def augment_from_permissions(cls):
|
||||
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
|
||||
@ -475,7 +539,7 @@ class PermissionOverwrite:
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_NAMES:
|
||||
raise ValueError('no permission called {0}.'.format(key))
|
||||
raise ValueError(f'no permission called {key}.')
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
@ -484,7 +548,7 @@ class PermissionOverwrite:
|
||||
|
||||
def _set(self, key, value):
|
||||
if value not in (True, None, False):
|
||||
raise TypeError('Expected bool or NoneType, received {0.__class__.__name__}'.format(value))
|
||||
raise TypeError(f'Expected bool or NoneType, received {value.__class__.__name__}')
|
||||
|
||||
if value is None:
|
||||
self._values.pop(key, None)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -313,7 +311,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
'-c:a', codec,
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
'-b:a', '%sk' % bitrate,
|
||||
'-b:a', f'{bitrate}k',
|
||||
'-loglevel', 'warning'))
|
||||
|
||||
if isinstance(options, str):
|
||||
@ -421,7 +419,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
if isinstance(method, str):
|
||||
probefunc = getattr(cls, '_probe_codec_' + method, None)
|
||||
if probefunc is None:
|
||||
raise AttributeError("Invalid probe method '%s'" % method)
|
||||
raise AttributeError(f"Invalid probe method {method!r}")
|
||||
|
||||
if probefunc is cls._probe_codec_native:
|
||||
fallback = cls._probe_codec_fallback
|
||||
@ -431,7 +429,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
fallback = cls._probe_codec_fallback
|
||||
else:
|
||||
raise TypeError("Expected str or callable for parameter 'probe', " \
|
||||
"not '{0.__class__.__name__}'" .format(method))
|
||||
f"not '{method.__class__.__name__}'")
|
||||
|
||||
codec = bitrate = None
|
||||
loop = asyncio.get_event_loop()
|
||||
@ -519,7 +517,7 @@ class PCMVolumeTransformer(AudioSource):
|
||||
|
||||
def __init__(self, original, volume=1.0):
|
||||
if not isinstance(original, AudioSource):
|
||||
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(original))
|
||||
raise TypeError(f'expected AudioSource not {original.__class__.__name__}.')
|
||||
|
||||
if original.is_opus():
|
||||
raise ClientException('AudioSource must not be Opus encoded.')
|
||||
@ -619,7 +617,7 @@ class AudioPlayer(threading.Thread):
|
||||
exc.__context__ = error
|
||||
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
||||
elif error:
|
||||
msg = 'Exception in voice thread {}'.format(self.name)
|
||||
msg = f'Exception in voice thread {self.name}'
|
||||
log.exception(msg, exc_info=error)
|
||||
print(msg, file=sys.stderr)
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,10 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'RawMessageDeleteEvent',
|
||||
'RawBulkMessageDeleteEvent',
|
||||
'RawMessageUpdateEvent',
|
||||
'RawReactionActionEvent',
|
||||
'RawReactionClearEvent',
|
||||
'RawReactionClearEmojiEvent',
|
||||
)
|
||||
|
||||
class _RawReprMixin:
|
||||
def __repr__(self):
|
||||
value = ' '.join('%s=%r' % (attr, getattr(self, attr)) for attr in self.__slots__)
|
||||
return '<%s %s>' % (self.__class__.__name__, value)
|
||||
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
|
||||
return f'<{self.__class__.__name__} {value}>'
|
||||
|
||||
class RawMessageDeleteEvent(_RawReprMixin):
|
||||
"""Represents the event payload for a :func:`on_raw_message_delete` event.
|
||||
@ -93,14 +100,19 @@ class RawMessageUpdateEvent(_RawReprMixin):
|
||||
The channel ID where the update took place.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the message got updated, if applicable.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
data: :class:`dict`
|
||||
The raw data given by the `gateway <https://discord.com/developers/docs/topics/gateway#message-update>`_
|
||||
cached_message: Optional[:class:`Message`]
|
||||
The cached message, if found in the internal message cache.
|
||||
The cached message, if found in the internal message cache. Represents the message before
|
||||
it is modified by the data in :attr:`RawMessageUpdateEvent.data`.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'data', 'cached_message')
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message')
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data['id'])
|
||||
@ -108,6 +120,11 @@ class RawMessageUpdateEvent(_RawReprMixin):
|
||||
self.data = data
|
||||
self.cached_message = None
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
|
||||
class RawReactionActionEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_reaction_add` or
|
||||
:func:`on_raw_reaction_remove` event.
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -26,6 +24,10 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from .iterators import ReactionIterator
|
||||
|
||||
__all__ = (
|
||||
'Reaction',
|
||||
)
|
||||
|
||||
class Reaction:
|
||||
"""Represents a reaction to a message.
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .enums import RelationshipType, try_enum
|
||||
|
||||
class Relationship:
|
||||
"""Represents a relationship in Discord.
|
||||
|
||||
A relationship is like a friendship, a person who is blocked, etc.
|
||||
Only non-bot accounts can have relationships.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
user: :class:`User`
|
||||
The user you have the relationship with.
|
||||
type: :class:`RelationshipType`
|
||||
The type of relationship you have.
|
||||
"""
|
||||
|
||||
__slots__ = ('type', 'user', '_state')
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.type = try_enum(RelationshipType, data['type'])
|
||||
self.user = state.store_user(data['user'])
|
||||
|
||||
def __repr__(self):
|
||||
return '<Relationship user={0.user!r} type={0.type!r}>'.format(self)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the relationship.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Deleting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.remove_relationship(self.user.id)
|
||||
|
||||
async def accept(self):
|
||||
"""|coro|
|
||||
|
||||
Accepts the relationship request. e.g. accepting a
|
||||
friend request.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Accepting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.user.id)
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -30,6 +28,11 @@ from .colour import Colour
|
||||
from .mixins import Hashable
|
||||
from .utils import snowflake_time, _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
'RoleTags',
|
||||
'Role',
|
||||
)
|
||||
|
||||
class RoleTags:
|
||||
"""Represents tags on a role.
|
||||
|
||||
@ -191,7 +194,7 @@ class Role(Hashable):
|
||||
|
||||
def _update(self, data):
|
||||
self.name = data['name']
|
||||
self._permissions = data.get('permissions', 0)
|
||||
self._permissions = int(data.get('permissions_new', 0))
|
||||
self.position = data.get('position', 0)
|
||||
self._colour = data.get('color', 0)
|
||||
self.hoist = data.get('hoist', False)
|
||||
@ -251,7 +254,7 @@ class Role(Hashable):
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention a role."""
|
||||
return '<@&%s>' % self.id
|
||||
return f'<@&{self.id}>'
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
@ -343,7 +346,7 @@ class Role(Hashable):
|
||||
|
||||
payload = {
|
||||
'name': fields.get('name', self.name),
|
||||
'permissions': fields.get('permissions', self.permissions).value,
|
||||
'permissions': str(fields.get('permissions', self.permissions).value),
|
||||
'color': colour.value,
|
||||
'hoist': fields.get('hoist', self.hoist),
|
||||
'mentionable': fields.get('mentionable', self.mentionable)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -46,6 +44,11 @@ from .errors import (
|
||||
from . import utils
|
||||
from .enums import Status
|
||||
|
||||
__all__ = (
|
||||
'AutoShardedClient',
|
||||
'ShardInfo',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventType:
|
||||
@ -319,7 +322,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
def _get_state(self, **options):
|
||||
return AutoShardedConnectionState(dispatch=self.dispatch,
|
||||
handlers=self._handlers, syncer=self._syncer,
|
||||
handlers=self._handlers,
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
|
||||
@property
|
||||
@ -413,7 +416,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
self._connection.shard_count = self.shard_count
|
||||
|
||||
shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count)
|
||||
shard_ids = self.shard_ids or range(self.shard_count)
|
||||
self._connection.shard_ids = shard_ids
|
||||
|
||||
for shard_id in shard_ids:
|
||||
|
177
discord/state.py
177
discord/state.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -44,7 +42,6 @@ from .emoji import Emoji
|
||||
from .mentions import AllowedMentions
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .message import Message
|
||||
from .relationship import Relationship
|
||||
from .channel import *
|
||||
from .raw_models import *
|
||||
from .member import Member
|
||||
@ -54,6 +51,7 @@ from . import utils
|
||||
from .flags import Intents, MemberCacheFlags
|
||||
from .object import Object
|
||||
from .invite import Invite
|
||||
from .interactions import Interaction
|
||||
|
||||
class ChunkRequest:
|
||||
def __init__(self, guild_id, loop, resolver, *, cache=True):
|
||||
@ -104,7 +102,7 @@ async def logging_coroutine(coroutine, *, info):
|
||||
log.exception('Exception occurred during %s', info)
|
||||
|
||||
class ConnectionState:
|
||||
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options):
|
||||
def __init__(self, *, dispatch, handlers, hooks, http, loop, **options):
|
||||
self.loop = loop
|
||||
self.http = http
|
||||
self.max_messages = options.get('max_messages', 1000)
|
||||
@ -112,12 +110,11 @@ class ConnectionState:
|
||||
self.max_messages = 1000
|
||||
|
||||
self.dispatch = dispatch
|
||||
self.syncer = syncer
|
||||
self.is_bot = None
|
||||
self.handlers = handlers
|
||||
self.hooks = hooks
|
||||
self.shard_count = None
|
||||
self._ready_task = None
|
||||
self.application_id = utils._get_as_snowflake(options, 'application_id')
|
||||
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
|
||||
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
|
||||
if self.guild_ready_timeout < 0:
|
||||
@ -149,7 +146,7 @@ class ConnectionState:
|
||||
intents = options.get('intents', None)
|
||||
if intents is not None:
|
||||
if not isinstance(intents, Intents):
|
||||
raise TypeError('intents parameter must be Intent not %r' % type(intents))
|
||||
raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
|
||||
else:
|
||||
intents = Intents.default()
|
||||
|
||||
@ -175,7 +172,7 @@ class ConnectionState:
|
||||
cache_flags = MemberCacheFlags.from_intents(intents)
|
||||
else:
|
||||
if not isinstance(cache_flags, MemberCacheFlags):
|
||||
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags))
|
||||
raise TypeError(f'member_cache_flags parameter must be MemberCacheFlags not {type(cache_flags)!r}')
|
||||
|
||||
cache_flags._verify_intents(intents)
|
||||
|
||||
@ -198,7 +195,6 @@ class ConnectionState:
|
||||
self.user = None
|
||||
self._users = weakref.WeakValueDictionary()
|
||||
self._emojis = {}
|
||||
self._calls = {}
|
||||
self._guilds = {}
|
||||
self._voice_clients = {}
|
||||
|
||||
@ -340,7 +336,7 @@ class ConnectionState:
|
||||
channel_id = channel.id
|
||||
self._private_channels[channel_id] = channel
|
||||
|
||||
if self.is_bot and len(self._private_channels) > 128:
|
||||
if len(self._private_channels) > 128:
|
||||
_, to_remove = self._private_channels.popitem(last=False)
|
||||
if isinstance(to_remove, DMChannel):
|
||||
self._private_channels_by_user.pop(to_remove.recipient.id, None)
|
||||
@ -405,36 +401,34 @@ class ConnectionState:
|
||||
|
||||
async def _delay_ready(self):
|
||||
try:
|
||||
# only real bots wait for GUILD_CREATE streaming
|
||||
if self.is_bot:
|
||||
states = []
|
||||
while True:
|
||||
# this snippet of code is basically waiting N seconds
|
||||
# until the last GUILD_CREATE was sent
|
||||
try:
|
||||
guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
states = []
|
||||
while True:
|
||||
# this snippet of code is basically waiting N seconds
|
||||
# until the last GUILD_CREATE was sent
|
||||
try:
|
||||
guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
else:
|
||||
if self._guild_needs_chunking(guild):
|
||||
future = await self.chunk_guild(guild, wait=False)
|
||||
states.append((guild, future))
|
||||
else:
|
||||
if self._guild_needs_chunking(guild):
|
||||
future = await self.chunk_guild(guild, wait=False)
|
||||
states.append((guild, future))
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
for guild, future in states:
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
||||
for guild, future in states:
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
||||
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
if guild.unavailable is False:
|
||||
self.dispatch('guild_available', guild)
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
# remove the state
|
||||
try:
|
||||
@ -442,10 +436,6 @@ class ConnectionState:
|
||||
except AttributeError:
|
||||
pass # already been deleted somehow
|
||||
|
||||
# call GUILD_SYNC after we're done chunking
|
||||
if not self.is_bot:
|
||||
log.info('Requesting GUILD_SYNC for %s guilds', len(self.guilds))
|
||||
await self.syncer([s.id for s in self.guilds])
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
else:
|
||||
@ -464,23 +454,19 @@ class ConnectionState:
|
||||
self.user = user = ClientUser(state=self, data=data['user'])
|
||||
self._users[user.id] = user
|
||||
|
||||
if self.application_id is None:
|
||||
try:
|
||||
application = data['application']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||
|
||||
for guild_data in data['guilds']:
|
||||
self._add_guild_from_data(guild_data)
|
||||
|
||||
for relationship in data.get('relationships', []):
|
||||
try:
|
||||
r_id = int(relationship['id'])
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
user._relationships[r_id] = Relationship(state=self, data=relationship)
|
||||
|
||||
for pm in data.get('private_channels', []):
|
||||
factory, _ = _channel_factory(pm['type'])
|
||||
self._add_private_channel(factory(me=user, data=pm, state=self))
|
||||
|
||||
self.dispatch('connect')
|
||||
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop)
|
||||
self._ready_task = asyncio.create_task(self._delay_ready())
|
||||
|
||||
def parse_resumed(self, data):
|
||||
self.dispatch('resumed')
|
||||
@ -601,6 +587,10 @@ class ConnectionState:
|
||||
if reaction:
|
||||
self.dispatch('reaction_clear_emoji', reaction)
|
||||
|
||||
def parse_interaction_create(self, data):
|
||||
interaction = Interaction(data=data, state=self)
|
||||
self.dispatch('interaction', interaction)
|
||||
|
||||
def parse_presence_update(self, data):
|
||||
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
guild = self._get_guild(guild_id)
|
||||
@ -724,22 +714,6 @@ class ConnectionState:
|
||||
else:
|
||||
self.dispatch('guild_channel_pins_update', channel, last_pin)
|
||||
|
||||
def parse_channel_recipient_add(self, data):
|
||||
channel = self._get_private_channel(int(data['channel_id']))
|
||||
user = self.store_user(data['user'])
|
||||
channel.recipients.append(user)
|
||||
self.dispatch('group_join', channel, user)
|
||||
|
||||
def parse_channel_recipient_remove(self, data):
|
||||
channel = self._get_private_channel(int(data['channel_id']))
|
||||
user = self.store_user(data['user'])
|
||||
try:
|
||||
channel.recipients.remove(user)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.dispatch('group_remove', channel, user)
|
||||
|
||||
def parse_guild_member_add(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
if guild is None:
|
||||
@ -868,7 +842,7 @@ class ConnectionState:
|
||||
|
||||
# check if it requires chunking
|
||||
if self._guild_needs_chunking(guild):
|
||||
asyncio.ensure_future(self._chunk_and_dispatch(guild, unavailable), loop=self.loop)
|
||||
asyncio.create_task(self._chunk_and_dispatch(guild, unavailable))
|
||||
return
|
||||
|
||||
# Dispatch available if newly available
|
||||
@ -877,10 +851,6 @@ class ConnectionState:
|
||||
else:
|
||||
self.dispatch('guild_join', guild)
|
||||
|
||||
def parse_guild_sync(self, data):
|
||||
guild = self._get_guild(int(data['id']))
|
||||
guild._sync(data)
|
||||
|
||||
def parse_guild_update(self, data):
|
||||
guild = self._get_guild(int(data['id']))
|
||||
if guild is not None:
|
||||
@ -896,7 +866,7 @@ class ConnectionState:
|
||||
log.debug('GUILD_DELETE referencing an unknown guild ID: %s. Discarding.', data['id'])
|
||||
return
|
||||
|
||||
if data.get('unavailable', False) and guild is not None:
|
||||
if data.get('unavailable', False):
|
||||
# GUILD_DELETE with unavailable being True means that the
|
||||
# guild that was available is now currently unavailable
|
||||
guild.unavailable = True
|
||||
@ -928,10 +898,9 @@ class ConnectionState:
|
||||
|
||||
def parse_guild_ban_remove(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
if guild is not None:
|
||||
if 'user' in data:
|
||||
user = self.store_user(data['user'])
|
||||
self.dispatch('member_unban', guild, user)
|
||||
if guild is not None and 'user' in data:
|
||||
user = self.store_user(data['user'])
|
||||
self.dispatch('member_unban', guild, user)
|
||||
|
||||
def parse_guild_role_create(self, data):
|
||||
guild = self._get_guild(int(data['guild_id']))
|
||||
@ -1013,7 +982,7 @@ class ConnectionState:
|
||||
voice = self._get_voice_client(guild.id)
|
||||
if voice is not None:
|
||||
coro = voice.on_voice_state_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
||||
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
||||
|
||||
member, before, after = guild._update_voice_state(data, channel_id)
|
||||
if member is not None:
|
||||
@ -1027,11 +996,6 @@ class ConnectionState:
|
||||
self.dispatch('voice_state_update', member, before, after)
|
||||
else:
|
||||
log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id'])
|
||||
else:
|
||||
# in here we're either at private or group calls
|
||||
call = self._calls.get(channel_id)
|
||||
if call is not None:
|
||||
call._update_voice_state(data)
|
||||
|
||||
def parse_voice_server_update(self, data):
|
||||
try:
|
||||
@ -1042,7 +1006,7 @@ class ConnectionState:
|
||||
vc = self._get_voice_client(key_id)
|
||||
if vc is not None:
|
||||
coro = vc.on_voice_server_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||
|
||||
def parse_typing_start(self, data):
|
||||
channel, guild = self._get_guild_channel(data)
|
||||
@ -1053,32 +1017,19 @@ class ConnectionState:
|
||||
member = channel.recipient
|
||||
elif isinstance(channel, TextChannel) and guild is not None:
|
||||
member = guild.get_member(user_id)
|
||||
if member is None:
|
||||
member_data = data.get('member')
|
||||
if member_data:
|
||||
member = Member(data=member_data, state=self, guild=guild)
|
||||
|
||||
elif isinstance(channel, GroupChannel):
|
||||
member = utils.find(lambda x: x.id == user_id, channel.recipients)
|
||||
|
||||
if member is not None:
|
||||
timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp'))
|
||||
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
|
||||
self.dispatch('typing', channel, member, timestamp)
|
||||
|
||||
def parse_relationship_add(self, data):
|
||||
key = int(data['id'])
|
||||
old = self.user.get_relationship(key)
|
||||
new = Relationship(state=self, data=data)
|
||||
self.user._relationships[key] = new
|
||||
if old is not None:
|
||||
self.dispatch('relationship_update', old, new)
|
||||
else:
|
||||
self.dispatch('relationship_add', new)
|
||||
|
||||
def parse_relationship_remove(self, data):
|
||||
key = int(data['id'])
|
||||
try:
|
||||
old = self.user._relationships.pop(key)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.dispatch('relationship_remove', old)
|
||||
|
||||
def _get_reaction_user(self, channel, user_id):
|
||||
if isinstance(channel, TextChannel):
|
||||
return channel.guild.get_member(user_id)
|
||||
@ -1216,16 +1167,20 @@ class AutoShardedConnectionState(ConnectionState):
|
||||
self.user = user = ClientUser(state=self, data=data['user'])
|
||||
self._users[user.id] = user
|
||||
|
||||
if self.application_id is None:
|
||||
try:
|
||||
application = data['application']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||
|
||||
for guild_data in data['guilds']:
|
||||
self._add_guild_from_data(guild_data)
|
||||
|
||||
if self._messages:
|
||||
self._update_message_references()
|
||||
|
||||
for pm in data.get('private_channels', []):
|
||||
factory, _ = _channel_factory(pm['type'])
|
||||
self._add_private_channel(factory(me=user, data=pm, state=self))
|
||||
|
||||
self.dispatch('connect')
|
||||
self.dispatch('shard_connect', data['__shard_id__'])
|
||||
|
||||
@ -1238,7 +1193,7 @@ class AutoShardedConnectionState(ConnectionState):
|
||||
gc.collect()
|
||||
|
||||
if self._ready_task is None:
|
||||
self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop)
|
||||
self._ready_task = asyncio.create_task(self._delay_ready())
|
||||
|
||||
def parse_resumed(self, data):
|
||||
self.dispatch('resumed')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -29,6 +27,10 @@ from .asset import Asset
|
||||
from .utils import snowflake_time
|
||||
from .enums import StickerType, try_enum
|
||||
|
||||
__all__ = (
|
||||
'Sticker',
|
||||
)
|
||||
|
||||
class Sticker(Hashable):
|
||||
"""Represents a sticker
|
||||
|
||||
@ -93,7 +95,7 @@ class Sticker(Hashable):
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC as a naive datetime."""
|
||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -43,10 +41,6 @@ class _PartialTemplateState:
|
||||
self.__state = state
|
||||
self.http = _FriendlyHttpAttributeErrorHelper()
|
||||
|
||||
@property
|
||||
def is_bot(self):
|
||||
return self.__state.is_bot
|
||||
|
||||
@property
|
||||
def shard_count(self):
|
||||
return self.__state.shard_count
|
||||
@ -76,7 +70,7 @@ class _PartialTemplateState:
|
||||
return []
|
||||
|
||||
def __getattr__(self, attr):
|
||||
raise AttributeError('PartialTemplateState does not support {0!r}.'.format(attr))
|
||||
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
|
||||
|
||||
class Template:
|
||||
"""Represents a Discord template.
|
||||
@ -96,16 +90,19 @@ class Template:
|
||||
creator: :class:`User`
|
||||
The creator of the template.
|
||||
created_at: :class:`datetime.datetime`
|
||||
When the template was created.
|
||||
An aware datetime in UTC representing when the template was created.
|
||||
updated_at: :class:`datetime.datetime`
|
||||
When the template was last updated (referred to as "last synced" in the client).
|
||||
An aware datetime in UTC representing when the template was last updated.
|
||||
This is referred to as "last synced" in the official Discord client.
|
||||
source_guild: :class:`Guild`
|
||||
The source guild.
|
||||
"""
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self._store(data)
|
||||
|
||||
def _store(self, data):
|
||||
self.code = data['code']
|
||||
self.uses = data['usage_count']
|
||||
self.name = data['name']
|
||||
@ -117,11 +114,16 @@ class Template:
|
||||
self.updated_at = parse_time(data.get('updated_at'))
|
||||
|
||||
id = _get_as_snowflake(data, 'source_guild_id')
|
||||
source_serialised = data['serialized_source_guild']
|
||||
source_serialised['id'] = id
|
||||
state = _PartialTemplateState(state=self._state)
|
||||
|
||||
self.source_guild = Guild(data=source_serialised, state=state)
|
||||
guild = self._state._get_guild(id)
|
||||
|
||||
if guild is None:
|
||||
source_serialised = data['serialized_source_guild']
|
||||
source_serialised['id'] = id
|
||||
state = _PartialTemplateState(state=self._state)
|
||||
guild = Guild(data=source_serialised, state=state)
|
||||
|
||||
self.source_guild = guild
|
||||
|
||||
def __repr__(self):
|
||||
return '<Template code={0.code!r} uses={0.uses} name={0.name!r}' \
|
||||
@ -147,9 +149,9 @@ class Template:
|
||||
|
||||
Raises
|
||||
------
|
||||
:exc:`.HTTPException`
|
||||
HTTPException
|
||||
Guild creation failed.
|
||||
:exc:`.InvalidArgument`
|
||||
InvalidArgument
|
||||
Invalid icon image format given. Must be PNG or JPG.
|
||||
|
||||
Returns
|
||||
@ -161,10 +163,81 @@ class Template:
|
||||
if icon is not None:
|
||||
icon = _bytes_to_base64_data(icon)
|
||||
|
||||
if region is None:
|
||||
region = VoiceRegion.us_west.value
|
||||
else:
|
||||
region = region.value
|
||||
region = region or VoiceRegion.us_west
|
||||
region_value = region.value
|
||||
|
||||
data = await self._state.http.create_from_template(self.code, name, region, icon)
|
||||
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
|
||||
return Guild(data=data, state=self._state)
|
||||
|
||||
async def sync(self):
|
||||
"""|coro|
|
||||
|
||||
Sync the template to the guild's current state.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
|
||||
data = await self._state.http.sync_template(self.source_guild.id, self.code)
|
||||
self._store(data)
|
||||
|
||||
async def edit(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Edit the template metadata.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: Optional[:class:`str`]
|
||||
The template's new name.
|
||||
description: Optional[:class:`str`]
|
||||
The template's description.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
|
||||
self._store(data)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Delete the template.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
||||
source guild to do this.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the template failed.
|
||||
Forbidden
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
await self._state.http.delete_template(self.source_guild.id, self.code)
|
||||
|
10
discord/types/__init__.py
Normal file
10
discord/types/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
discord.types
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Typings for the Discord API
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
91
discord/types/channel.py
Normal file
91
discord/types/channel.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .user import PartialUser
|
||||
from .snowflake import Snowflake
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
|
||||
class PermissionOverwrite(TypedDict):
|
||||
id: Snowflake
|
||||
type: Literal[0, 1]
|
||||
allow: str
|
||||
deny: str
|
||||
|
||||
|
||||
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
|
||||
|
||||
|
||||
class PartialChannel(TypedDict):
|
||||
id: str
|
||||
type: ChannelType
|
||||
name: str
|
||||
|
||||
|
||||
class _TextChannelOptional(PartialChannel, total=False):
|
||||
topic: str
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: int
|
||||
rate_limit_per_user: int
|
||||
|
||||
|
||||
class _VoiceChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
|
||||
|
||||
class _CategoryChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StoreChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StageChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
topic: str
|
||||
|
||||
|
||||
class GuildChannel(
|
||||
_TextChannelOptional, _VoiceChannelOptional, _CategoryChannelOptional, _StoreChannelOptional, _StageChannelOptional
|
||||
):
|
||||
guild_id: Snowflake
|
||||
position: int
|
||||
permission_overwrites: List[PermissionOverwrite]
|
||||
nsfw: bool
|
||||
parent_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class DMChannel(PartialChannel):
|
||||
last_message_id: Optional[Snowflake]
|
||||
recipients: List[PartialUser]
|
||||
|
||||
|
||||
class GroupDMChannel(DMChannel):
|
||||
icon: Optional[str]
|
||||
owner_id: Snowflake
|
84
discord/types/embed.py
Normal file
84
discord/types/embed.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import List, Literal, TypedDict
|
||||
|
||||
class _EmbedFooterOptional(TypedDict, total=False):
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
class EmbedFooter(_EmbedFooterOptional):
|
||||
text: str
|
||||
|
||||
class _EmbedFieldOptional(TypedDict, total=False):
|
||||
inline: bool
|
||||
|
||||
class EmbedField(_EmbedFieldOptional):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
class EmbedThumbnail(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedVideo(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedImage(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedProvider(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
class EmbedAuthor(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
|
||||
|
||||
class Embed(TypedDict, total=False):
|
||||
title: str
|
||||
type: EmbedType
|
||||
description: str
|
||||
url: str
|
||||
timestamp: str
|
||||
color: int
|
||||
footer: EmbedFooter
|
||||
image: EmbedImage
|
||||
thumbnail: EmbedThumbnail
|
||||
video: EmbedVideo
|
||||
provider: EmbedProvider
|
||||
author: EmbedAuthor
|
||||
fields: List[EmbedField]
|
28
discord/types/snowflake.py
Normal file
28
discord/types/snowflake.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
Snowflake = str
|
||||
SnowflakeList = List[Snowflake]
|
33
discord/types/user.py
Normal file
33
discord/types/user.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .snowflake import Snowflake
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
|
||||
class PartialUser(TypedDict):
|
||||
id: Snowflake
|
||||
username: str
|
||||
discriminator: str
|
||||
avatar: Optional[str]
|
541
discord/user.py
541
discord/user.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -24,61 +22,17 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import discord.abc
|
||||
from .flags import PublicUserFlags
|
||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time
|
||||
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse, PremiumType, try_enum
|
||||
from .errors import ClientException
|
||||
from .utils import snowflake_time, _bytes_to_base64_data
|
||||
from .enums import DefaultAvatar, try_enum
|
||||
from .colour import Colour
|
||||
from .asset import Asset
|
||||
|
||||
class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')):
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
def nitro(self):
|
||||
return self.premium_since is not None
|
||||
|
||||
premium = nitro
|
||||
|
||||
def _has_flag(self, o):
|
||||
v = o.value
|
||||
return (self.flags & v) == v
|
||||
|
||||
@property
|
||||
def staff(self):
|
||||
return self._has_flag(UserFlags.staff)
|
||||
|
||||
@property
|
||||
def partner(self):
|
||||
return self._has_flag(UserFlags.partner)
|
||||
|
||||
@property
|
||||
def bug_hunter(self):
|
||||
return self._has_flag(UserFlags.bug_hunter)
|
||||
|
||||
@property
|
||||
def early_supporter(self):
|
||||
return self._has_flag(UserFlags.early_supporter)
|
||||
|
||||
@property
|
||||
def hypesquad(self):
|
||||
return self._has_flag(UserFlags.hypesquad)
|
||||
|
||||
@property
|
||||
def hypesquad_houses(self):
|
||||
flags = (UserFlags.hypesquad_bravery, UserFlags.hypesquad_brilliance, UserFlags.hypesquad_balance)
|
||||
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
|
||||
|
||||
@property
|
||||
def team_user(self):
|
||||
return self._has_flag(UserFlags.team_user)
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
return self._has_flag(UserFlags.system)
|
||||
__all__ = (
|
||||
'User',
|
||||
'ClientUser',
|
||||
)
|
||||
|
||||
_BaseUser = discord.abc.User
|
||||
|
||||
@ -123,6 +77,7 @@ class BaseUser(_BaseUser):
|
||||
self.avatar = user.avatar
|
||||
self.bot = user.bot
|
||||
self._state = user._state
|
||||
self._public_flags = user._public_flags
|
||||
|
||||
return self
|
||||
|
||||
@ -197,7 +152,7 @@ class BaseUser(_BaseUser):
|
||||
@property
|
||||
def default_avatar_url(self):
|
||||
""":class:`Asset`: Returns a URL for a user's default avatar."""
|
||||
return Asset(self._state, '/embed/avatars/{}.png'.format(self.default_avatar.value))
|
||||
return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
@ -220,7 +175,7 @@ class BaseUser(_BaseUser):
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||
return '<@{0.id}>'.format(self)
|
||||
return f'<@{self.id}>'
|
||||
|
||||
def permissions_in(self, channel):
|
||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
||||
@ -242,7 +197,8 @@ class BaseUser(_BaseUser):
|
||||
def created_at(self):
|
||||
""":class:`datetime.datetime`: Returns the user's creation time in UTC.
|
||||
|
||||
This is when the user's Discord account was created."""
|
||||
This is when the user's Discord account was created.
|
||||
"""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
@ -272,11 +228,7 @@ class BaseUser(_BaseUser):
|
||||
if message.mention_everyone:
|
||||
return True
|
||||
|
||||
for user in message.mentions:
|
||||
if user.id == self.id:
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(user.id == self.id for user in message.mentions)
|
||||
|
||||
class ClientUser(BaseUser):
|
||||
"""Represents your Discord user.
|
||||
@ -318,24 +270,16 @@ class ClientUser(BaseUser):
|
||||
|
||||
verified: :class:`bool`
|
||||
Specifies if the user is a verified account.
|
||||
email: Optional[:class:`str`]
|
||||
The email the user used when registering.
|
||||
locale: Optional[:class:`str`]
|
||||
The IETF language tag used to identify the language the user is using.
|
||||
mfa_enabled: :class:`bool`
|
||||
Specifies if the user has MFA turned on and working.
|
||||
premium: :class:`bool`
|
||||
Specifies if the user is a premium user (e.g. has Discord Nitro).
|
||||
premium_type: Optional[:class:`PremiumType`]
|
||||
Specifies the type of premium a user has (e.g. Nitro or Nitro Classic). Could be None if the user is not premium.
|
||||
"""
|
||||
__slots__ = BaseUser.__slots__ + \
|
||||
('email', 'locale', '_flags', 'verified', 'mfa_enabled',
|
||||
'premium', 'premium_type', '_relationships', '__weakref__')
|
||||
('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
super().__init__(state=state, data=data)
|
||||
self._relationships = {}
|
||||
|
||||
def __repr__(self):
|
||||
return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
|
||||
@ -345,70 +289,16 @@ class ClientUser(BaseUser):
|
||||
super()._update(data)
|
||||
# There's actually an Optional[str] phone field as well but I won't use it
|
||||
self.verified = data.get('verified', False)
|
||||
self.email = data.get('email')
|
||||
self.locale = data.get('locale')
|
||||
self._flags = data.get('flags', 0)
|
||||
self.mfa_enabled = data.get('mfa_enabled', False)
|
||||
self.premium = data.get('premium', False)
|
||||
self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
|
||||
|
||||
def get_relationship(self, user_id):
|
||||
"""Retrieves the :class:`Relationship` if applicable.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user_id: :class:`int`
|
||||
The user ID to check if we have a relationship with them.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Relationship`]
|
||||
The relationship if available or ``None``.
|
||||
"""
|
||||
return self._relationships.get(user_id)
|
||||
|
||||
@property
|
||||
def relationships(self):
|
||||
"""List[:class:`User`]: Returns all the relationships that the user has.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return list(self._relationships.values())
|
||||
|
||||
@property
|
||||
def friends(self):
|
||||
r"""List[:class:`User`]: Returns all the users that the user is friends with.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend]
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
r"""List[:class:`User`]: Returns all the users that the user has blocked.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked]
|
||||
|
||||
async def edit(self, **fields):
|
||||
async def edit(self, *, username=None, avatar=None):
|
||||
"""|coro|
|
||||
|
||||
Edits the current profile of the client.
|
||||
|
||||
If a bot account is used then a password field is optional,
|
||||
otherwise it is required.
|
||||
|
||||
.. note::
|
||||
|
||||
To upload an avatar, a :term:`py:bytes-like object` must be passed in that
|
||||
@ -420,19 +310,6 @@ class ClientUser(BaseUser):
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
password: :class:`str`
|
||||
The current password for the client's account.
|
||||
Only applicable to user accounts.
|
||||
new_password: :class:`str`
|
||||
The new password you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
email: :class:`str`
|
||||
The new email you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
house: Optional[:class:`HypeSquadHouse`]
|
||||
The hypesquad house you wish to change to.
|
||||
Could be ``None`` to leave the current house.
|
||||
Only applicable to user accounts.
|
||||
username: :class:`str`
|
||||
The new username you wish to change to.
|
||||
avatar: :class:`bytes`
|
||||
@ -445,212 +322,14 @@ class ClientUser(BaseUser):
|
||||
Editing your profile failed.
|
||||
InvalidArgument
|
||||
Wrong image format passed for ``avatar``.
|
||||
ClientException
|
||||
Password is required for non-bot accounts.
|
||||
House field was not a HypeSquadHouse.
|
||||
"""
|
||||
|
||||
try:
|
||||
avatar_bytes = fields['avatar']
|
||||
except KeyError:
|
||||
avatar = self.avatar
|
||||
else:
|
||||
if avatar_bytes is not None:
|
||||
avatar = _bytes_to_base64_data(avatar_bytes)
|
||||
else:
|
||||
avatar = None
|
||||
|
||||
not_bot_account = not self.bot
|
||||
password = fields.get('password')
|
||||
if not_bot_account and password is None:
|
||||
raise ClientException('Password is required for non-bot accounts.')
|
||||
|
||||
args = {
|
||||
'password': password,
|
||||
'username': fields.get('username', self.name),
|
||||
'avatar': avatar
|
||||
}
|
||||
|
||||
if not_bot_account:
|
||||
args['email'] = fields.get('email', self.email)
|
||||
|
||||
if 'new_password' in fields:
|
||||
args['new_password'] = fields['new_password']
|
||||
|
||||
http = self._state.http
|
||||
|
||||
if 'house' in fields:
|
||||
house = fields['house']
|
||||
if house is None:
|
||||
await http.leave_hypesquad_house()
|
||||
elif not isinstance(house, HypeSquadHouse):
|
||||
raise ClientException('`house` parameter was not a HypeSquadHouse')
|
||||
else:
|
||||
value = house.value
|
||||
|
||||
await http.change_hypesquad_house(value)
|
||||
|
||||
data = await http.edit_profile(**args)
|
||||
if not_bot_account:
|
||||
self.email = data['email']
|
||||
try:
|
||||
http._token(data['token'], bot=False)
|
||||
except KeyError:
|
||||
pass
|
||||
if avatar is not None:
|
||||
avatar = _bytes_to_base64_data(avatar)
|
||||
|
||||
data = await self._state.http.edit_profile(username=username, avatar=avatar)
|
||||
self._update(data)
|
||||
|
||||
async def create_group(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Creates a group direct message with the recipients
|
||||
provided. These recipients must be have a relationship
|
||||
of type :attr:`RelationshipType.friend`.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument :class:`list` of :class:`User` to have in
|
||||
your group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Failed to create the group direct message.
|
||||
ClientException
|
||||
Attempted to create a group with only one recipient.
|
||||
This does not include yourself.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`GroupChannel`
|
||||
The new group channel.
|
||||
"""
|
||||
|
||||
from .channel import GroupChannel
|
||||
|
||||
if len(recipients) < 2:
|
||||
raise ClientException('You must have two or more recipients to create a group.')
|
||||
|
||||
users = [str(u.id) for u in recipients]
|
||||
data = await self._state.http.start_group(self.id, users)
|
||||
return GroupChannel(me=self, data=data, state=self._state)
|
||||
|
||||
async def edit_settings(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Edits the client user's settings.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Parameters
|
||||
-------
|
||||
afk_timeout: :class:`int`
|
||||
How long (in seconds) the user needs to be AFK until Discord
|
||||
sends push notifications to your mobile device.
|
||||
animate_emojis: :class:`bool`
|
||||
Whether or not to animate emojis in the chat.
|
||||
convert_emoticons: :class:`bool`
|
||||
Whether or not to automatically convert emoticons into emojis.
|
||||
e.g. :-) -> 😃
|
||||
default_guilds_restricted: :class:`bool`
|
||||
Whether or not to automatically disable DMs between you and
|
||||
members of new guilds you join.
|
||||
detect_platform_accounts: :class:`bool`
|
||||
Whether or not to automatically detect accounts from services
|
||||
like Steam and Blizzard when you open the Discord client.
|
||||
developer_mode: :class:`bool`
|
||||
Whether or not to enable developer mode.
|
||||
disable_games_tab: :class:`bool`
|
||||
Whether or not to disable the showing of the Games tab.
|
||||
enable_tts_command: :class:`bool`
|
||||
Whether or not to allow tts messages to be played/sent.
|
||||
explicit_content_filter: :class:`UserContentFilter`
|
||||
The filter for explicit content in all messages.
|
||||
friend_source_flags: :class:`FriendFlags`
|
||||
Who can add you as a friend.
|
||||
gif_auto_play: :class:`bool`
|
||||
Whether or not to automatically play gifs that are in the chat.
|
||||
guild_positions: List[:class:`abc.Snowflake`]
|
||||
A list of guilds in order of the guild/guild icons that are on
|
||||
the left hand side of the UI.
|
||||
inline_attachment_media: :class:`bool`
|
||||
Whether or not to display attachments when they are uploaded in chat.
|
||||
inline_embed_media: :class:`bool`
|
||||
Whether or not to display videos and images from links posted in chat.
|
||||
locale: :class:`str`
|
||||
The :rfc:`3066` language identifier of the locale to use for the language
|
||||
of the Discord client.
|
||||
message_display_compact: :class:`bool`
|
||||
Whether or not to use the compact Discord display mode.
|
||||
render_embeds: :class:`bool`
|
||||
Whether or not to render embeds that are sent in the chat.
|
||||
render_reactions: :class:`bool`
|
||||
Whether or not to render reactions that are added to messages.
|
||||
restricted_guilds: List[:class:`abc.Snowflake`]
|
||||
A list of guilds that you will not receive DMs from.
|
||||
show_current_game: :class:`bool`
|
||||
Whether or not to display the game that you are currently playing.
|
||||
status: :class:`Status`
|
||||
The clients status that is shown to others.
|
||||
theme: :class:`Theme`
|
||||
The theme of the Discord UI.
|
||||
timezone_offset: :class:`int`
|
||||
The timezone offset to use.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the settings failed.
|
||||
Forbidden
|
||||
The client is a bot user and not a user account.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`dict`
|
||||
The client user's updated settings.
|
||||
"""
|
||||
payload = {}
|
||||
|
||||
content_filter = kwargs.pop('explicit_content_filter', None)
|
||||
if content_filter:
|
||||
payload.update({'explicit_content_filter': content_filter.value})
|
||||
|
||||
friend_flags = kwargs.pop('friend_source_flags', None)
|
||||
if friend_flags:
|
||||
dicts = [{}, {'mutual_guilds': True}, {'mutual_friends': True},
|
||||
{'mutual_guilds': True, 'mutual_friends': True}, {'all': True}]
|
||||
payload.update({'friend_source_flags': dicts[friend_flags.value]})
|
||||
|
||||
guild_positions = kwargs.pop('guild_positions', None)
|
||||
if guild_positions:
|
||||
guild_positions = [str(x.id) for x in guild_positions]
|
||||
payload.update({'guild_positions': guild_positions})
|
||||
|
||||
restricted_guilds = kwargs.pop('restricted_guilds', None)
|
||||
if restricted_guilds:
|
||||
restricted_guilds = [str(x.id) for x in restricted_guilds]
|
||||
payload.update({'restricted_guilds': restricted_guilds})
|
||||
|
||||
status = kwargs.pop('status', None)
|
||||
if status:
|
||||
payload.update({'status': status.value})
|
||||
|
||||
theme = kwargs.pop('theme', None)
|
||||
if theme:
|
||||
payload.update({'theme': theme.value})
|
||||
|
||||
payload.update(kwargs)
|
||||
|
||||
data = await self._state.http.edit_settings(**payload)
|
||||
return data
|
||||
|
||||
class User(BaseUser, discord.abc.Messageable):
|
||||
"""Represents a Discord user.
|
||||
|
||||
@ -706,8 +385,22 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
"""
|
||||
return self._state._get_private_channel_by_user(self.id)
|
||||
|
||||
@property
|
||||
def mutual_guilds(self):
|
||||
"""List[:class:`Guild`]: The guilds that the user shares with the client.
|
||||
|
||||
.. note::
|
||||
|
||||
This will only return mutual guilds within the client's internal cache.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return [guild for guild in self._state._guilds.values() if guild.get_member(self.id)]
|
||||
|
||||
async def create_dm(self):
|
||||
"""Creates a :class:`DMChannel` with this user.
|
||||
"""|coro|
|
||||
|
||||
Creates a :class:`DMChannel` with this user.
|
||||
|
||||
This should be rarely called, as this is done transparently for most
|
||||
people.
|
||||
@ -724,171 +417,3 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
state = self._state
|
||||
data = await state.http.start_private_message(self.id)
|
||||
return state.add_dm_channel(data)
|
||||
|
||||
@property
|
||||
def relationship(self):
|
||||
"""Optional[:class:`Relationship`]: Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
return self._state.user.get_relationship(self.id)
|
||||
|
||||
async def mutual_friends(self):
|
||||
"""|coro|
|
||||
|
||||
Gets all mutual friends of this user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to get mutual friends of this user.
|
||||
HTTPException
|
||||
Getting mutual friends failed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[:class:`User`]
|
||||
The users that are mutual friends.
|
||||
"""
|
||||
state = self._state
|
||||
mutuals = await state.http.get_mutual_friends(self.id)
|
||||
return [User(state=state, data=friend) for friend in mutuals]
|
||||
|
||||
def is_friend(self):
|
||||
""":class:`bool`: Checks if the user is your friend.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.friend
|
||||
|
||||
def is_blocked(self):
|
||||
""":class:`bool`: Checks if the user is blocked.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
"""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.blocked
|
||||
|
||||
async def block(self):
|
||||
"""|coro|
|
||||
|
||||
Blocks the user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to block this user.
|
||||
HTTPException
|
||||
Blocking the user failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
|
||||
|
||||
async def unblock(self):
|
||||
"""|coro|
|
||||
|
||||
Unblocks the user.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to unblock this user.
|
||||
HTTPException
|
||||
Unblocking the user failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def remove_friend(self):
|
||||
"""|coro|
|
||||
|
||||
Removes the user as a friend.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to remove this user as a friend.
|
||||
HTTPException
|
||||
Removing the user as a friend failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def send_friend_request(self):
|
||||
"""|coro|
|
||||
|
||||
Sends the user a friend request.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to send a friend request to the user.
|
||||
HTTPException
|
||||
Sending the friend request failed.
|
||||
"""
|
||||
await self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator)
|
||||
|
||||
async def profile(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the user's profile.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to fetch profiles.
|
||||
HTTPException
|
||||
Fetching the profile failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Profile`
|
||||
The profile of the user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
data = await state.http.get_user_profile(self.id)
|
||||
|
||||
def transform(d):
|
||||
return state._get_guild(int(d['id']))
|
||||
|
||||
since = data.get('premium_since')
|
||||
mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', []))))
|
||||
return Profile(flags=data['user'].get('flags', 0),
|
||||
premium_since=parse_time(since),
|
||||
mutual_guilds=mutual_guilds,
|
||||
user=self,
|
||||
connected_accounts=data['connected_accounts'])
|
||||
|
152
discord/utils.py
152
discord/utils.py
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -27,12 +25,13 @@ DEALINGS IN THE SOFTWARE.
|
||||
import array
|
||||
import asyncio
|
||||
import collections.abc
|
||||
from typing import Optional, overload
|
||||
import unicodedata
|
||||
from base64 import b64encode
|
||||
from bisect import bisect_left
|
||||
import datetime
|
||||
import functools
|
||||
from inspect import isawaitable as _isawaitable
|
||||
from inspect import isawaitable as _isawaitable, signature as _signature
|
||||
from operator import attrgetter
|
||||
import json
|
||||
import re
|
||||
@ -40,8 +39,19 @@ import warnings
|
||||
|
||||
from .errors import InvalidArgument
|
||||
|
||||
__all__ = (
|
||||
'oauth_uri',
|
||||
'snowflake_time',
|
||||
'time_snowflake',
|
||||
'find',
|
||||
'get',
|
||||
'sleep_until',
|
||||
'utcnow',
|
||||
'remove_markdown',
|
||||
'escape_markdown',
|
||||
'escape_mentions',
|
||||
)
|
||||
DISCORD_EPOCH = 1420070400000
|
||||
MAX_ASYNCIO_SECONDS = 3456000
|
||||
|
||||
class cached_property:
|
||||
def __init__(self, function):
|
||||
@ -105,11 +115,26 @@ class SequenceProxy(collections.abc.Sequence):
|
||||
def count(self, value):
|
||||
return self.__proxied.count(value)
|
||||
|
||||
def parse_time(timestamp):
|
||||
@overload
|
||||
def parse_time(timestamp: None) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_time(timestamp: str) -> datetime.datetime:
|
||||
...
|
||||
|
||||
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
|
||||
if timestamp:
|
||||
return datetime.datetime(*map(int, re.split(r'[^\d]', timestamp.replace('+00:00', ''))))
|
||||
return datetime.datetime.fromisoformat(timestamp)
|
||||
return None
|
||||
|
||||
def copy_doc(original):
|
||||
def decorator(overriden):
|
||||
overriden.__doc__ = original.__doc__
|
||||
overriden.__signature__ = _signature(original)
|
||||
return overriden
|
||||
return decorator
|
||||
|
||||
def deprecated(instead=None):
|
||||
def actual_decorator(func):
|
||||
@functools.wraps(func)
|
||||
@ -126,7 +151,7 @@ def deprecated(instead=None):
|
||||
return decorated
|
||||
return actual_decorator
|
||||
|
||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes=None):
|
||||
"""A helper function that returns the OAuth2 URL for inviting the bot
|
||||
into guilds.
|
||||
|
||||
@ -141,13 +166,18 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
The guild to pre-select in the authorization screen, if available.
|
||||
redirect_uri: :class:`str`
|
||||
An optional valid redirect URI.
|
||||
scopes: Iterable[:class:`str`]
|
||||
An optional valid list of scopes. Defaults to ``('bot',)``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The OAuth2 URL for inviting the bot into guilds.
|
||||
"""
|
||||
url = 'https://discord.com/oauth2/authorize?client_id={}&scope=bot'.format(client_id)
|
||||
url = f'https://discord.com/oauth2/authorize?client_id={client_id}'
|
||||
url = url + '&scope=' + '+'.join(scopes or ('bot',))
|
||||
if permissions is not None:
|
||||
url = url + '&permissions=' + str(permissions.value)
|
||||
if guild is not None:
|
||||
@ -158,7 +188,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
return url
|
||||
|
||||
|
||||
def snowflake_time(id):
|
||||
def snowflake_time(id: int) -> datetime.datetime:
|
||||
"""
|
||||
Parameters
|
||||
-----------
|
||||
@ -168,25 +198,34 @@ def snowflake_time(id):
|
||||
Returns
|
||||
--------
|
||||
:class:`datetime.datetime`
|
||||
The creation date in UTC of a Discord snowflake ID."""
|
||||
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000)
|
||||
An aware datetime in UTC representing the creation time of the snowflake.
|
||||
"""
|
||||
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
def time_snowflake(datetime_obj, high=False):
|
||||
def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
|
||||
"""Returns a numeric snowflake pretending to be created at the given date.
|
||||
|
||||
When using as the lower end of a range, use ``time_snowflake(high=False) - 1`` to be inclusive, ``high=True`` to be exclusive
|
||||
When using as the higher end of a range, use ``time_snowflake(high=True)`` + 1 to be inclusive, ``high=False`` to be exclusive
|
||||
When using as the lower end of a range, use ``time_snowflake(high=False) - 1``
|
||||
to be inclusive, ``high=True`` to be exclusive.
|
||||
|
||||
When using as the higher end of a range, use ``time_snowflake(high=True) + 1``
|
||||
to be inclusive, ``high=False`` to be exclusive
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
datetime_obj: :class:`datetime.datetime`
|
||||
A timezone-naive datetime object representing UTC time.
|
||||
dt: :class:`datetime.datetime`
|
||||
A datetime object to convert to a snowflake.
|
||||
If naive, the timezone is assumed to be local time.
|
||||
high: :class:`bool`
|
||||
Whether or not to set the lower 22 bit to high or low.
|
||||
"""
|
||||
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
|
||||
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The snowflake representing the time given.
|
||||
"""
|
||||
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
|
||||
return (discord_millis << 22) + (2**22-1 if high else 0)
|
||||
|
||||
def find(predicate, seq):
|
||||
@ -364,19 +403,31 @@ async def sleep_until(when, result=None):
|
||||
-----------
|
||||
when: :class:`datetime.datetime`
|
||||
The timestamp in which to sleep until. If the datetime is naive then
|
||||
it is assumed to be in UTC.
|
||||
it is assumed to be local time.
|
||||
result: Any
|
||||
If provided is returned to the caller when the coroutine completes.
|
||||
"""
|
||||
if when.tzinfo is None:
|
||||
when = when.replace(tzinfo=datetime.timezone.utc)
|
||||
when = when.astimezone()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
delta = (when - now).total_seconds()
|
||||
while delta > MAX_ASYNCIO_SECONDS:
|
||||
await asyncio.sleep(MAX_ASYNCIO_SECONDS)
|
||||
delta -= MAX_ASYNCIO_SECONDS
|
||||
return await asyncio.sleep(max(delta, 0), result)
|
||||
|
||||
def utcnow() -> datetime.datetime:
|
||||
"""A helper function to return an aware UTC datetime representing the current time.
|
||||
|
||||
This should be preferred to :func:`datetime.datetime.utcnow` since it is an aware
|
||||
datetime, compared to the naive datetime in the standard library.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`datetime.datetime`
|
||||
The current aware datetime in UTC.
|
||||
"""
|
||||
return datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
def valid_icon_size(size):
|
||||
"""Icons must be power of 2 within [16, 4096]."""
|
||||
return not size & (size - 1) and size in range(16, 4097)
|
||||
@ -419,11 +470,8 @@ def _string_width(string, *, _IS_ASCII=_IS_ASCII):
|
||||
return match.endpos
|
||||
|
||||
UNICODE_WIDE_CHAR_TYPE = 'WFA'
|
||||
width = 0
|
||||
func = unicodedata.east_asian_width
|
||||
for char in string:
|
||||
width += 2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1
|
||||
return width
|
||||
return sum(2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1 for char in string)
|
||||
|
||||
def resolve_invite(invite):
|
||||
"""
|
||||
@ -480,7 +528,44 @@ _MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(
|
||||
|
||||
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
|
||||
|
||||
_MARKDOWN_ESCAPE_REGEX = re.compile(r'(?P<markdown>%s|%s)' % (_MARKDOWN_ESCAPE_SUBREGEX, _MARKDOWN_ESCAPE_COMMON), re.MULTILINE)
|
||||
_MARKDOWN_ESCAPE_REGEX = re.compile(fr'(?P<markdown>{_MARKDOWN_ESCAPE_SUBREGEX}|{_MARKDOWN_ESCAPE_COMMON})', re.MULTILINE)
|
||||
|
||||
_URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
|
||||
|
||||
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
|
||||
|
||||
def remove_markdown(text, *, ignore_links=True):
|
||||
"""A helper function that removes markdown characters.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. note::
|
||||
This function is not markdown aware and may remove meaning from the original text. For example,
|
||||
if the input contains ``10 * 5`` then it will be converted into ``10 5``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
text: :class:`str`
|
||||
The text to remove markdown from.
|
||||
ignore_links: :class:`bool`
|
||||
Whether to leave links alone when removing markdown. For example,
|
||||
if a URL in the text contains characters such as ``_`` then it will
|
||||
be left alone. Defaults to ``True``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The text with the markdown special characters removed.
|
||||
"""
|
||||
|
||||
def replacement(match):
|
||||
groupdict = match.groupdict()
|
||||
return groupdict.get('url', '')
|
||||
|
||||
regex = _MARKDOWN_STOCK_REGEX
|
||||
if ignore_links:
|
||||
regex = f'(?:{_URL_REGEX}|{regex})'
|
||||
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
||||
|
||||
def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
r"""A helper function that escapes Discord's markdown.
|
||||
@ -508,7 +593,6 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
"""
|
||||
|
||||
if not as_needed:
|
||||
url_regex = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
|
||||
def replacement(match):
|
||||
groupdict = match.groupdict()
|
||||
is_url = groupdict.get('url')
|
||||
@ -516,9 +600,9 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
||||
return is_url
|
||||
return '\\' + groupdict['markdown']
|
||||
|
||||
regex = r'(?P<markdown>[_\\~|\*`]|%s)' % _MARKDOWN_ESCAPE_COMMON
|
||||
regex = _MARKDOWN_STOCK_REGEX
|
||||
if ignore_links:
|
||||
regex = '(?:%s|%s)' % (url_regex, regex)
|
||||
regex = f'(?:{_URL_REGEX}|{regex})'
|
||||
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
||||
else:
|
||||
text = re.sub(r'\\', r'\\\\', text)
|
||||
@ -547,4 +631,4 @@ def escape_mentions(text):
|
||||
:class:`str`
|
||||
The text with the mentions removed.
|
||||
"""
|
||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,21})', '@\u200b\\1', text)
|
||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,20})', '@\u200b\\1', text)
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -57,6 +55,11 @@ try:
|
||||
except ImportError:
|
||||
has_nacl = False
|
||||
|
||||
__all__ = (
|
||||
'VoiceProtocol',
|
||||
'VoiceClient',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class VoiceProtocol:
|
||||
@ -558,7 +561,7 @@ class VoiceClient(VoiceProtocol):
|
||||
raise ClientException('Already playing audio.')
|
||||
|
||||
if not isinstance(source, AudioSource):
|
||||
raise TypeError('source must an AudioSource not {0.__class__.__name__}'.format(source))
|
||||
raise TypeError(f'source must an AudioSource not {source.__class__.__name__}')
|
||||
|
||||
if not self.encoder and not source.is_opus():
|
||||
self.encoder = opus.Encoder()
|
||||
@ -601,7 +604,7 @@ class VoiceClient(VoiceProtocol):
|
||||
@source.setter
|
||||
def source(self, value):
|
||||
if not isinstance(value, AudioSource):
|
||||
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(value))
|
||||
raise TypeError(f'expected AudioSource not {value.__class__.__name__}.')
|
||||
|
||||
if self._player is None:
|
||||
raise ValueError('Not playing anything.')
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -47,10 +45,104 @@ __all__ = (
|
||||
'RequestsWebhookAdapter',
|
||||
'Webhook',
|
||||
'WebhookMessage',
|
||||
'PartialWebhookChannel',
|
||||
'PartialWebhookGuild'
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PartialWebhookChannel(Hashable):
|
||||
"""Represents a partial channel for webhooks.
|
||||
|
||||
These are typically given for channel follower webhooks.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The partial channel's ID.
|
||||
name: :class:`str`
|
||||
The partial channel's name.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name')
|
||||
|
||||
def __init__(self, *, data):
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
|
||||
def __repr__(self):
|
||||
return f'<PartialWebhookChannel name={self.name!r} id={self.id}>'
|
||||
|
||||
class PartialWebhookGuild(Hashable):
|
||||
"""Represents a partial guild for webhooks.
|
||||
|
||||
These are typically given for channel follower webhooks.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The partial guild's ID.
|
||||
name: :class:`str`
|
||||
The partial guild's name.
|
||||
icon: :class:`str`
|
||||
The partial guild's icon
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name', 'icon', '_state')
|
||||
|
||||
def __init__(self, *, data, state):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.icon = data['icon']
|
||||
|
||||
def __repr__(self):
|
||||
return f'<PartialWebhookGuild name={self.name!r} id={self.id}>'
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`Asset`: Returns the guild's icon asset."""
|
||||
return self.icon_url_as()
|
||||
|
||||
def is_icon_animated(self):
|
||||
""":class:`bool`: Returns True if the guild has an animated icon."""
|
||||
return bool(self.icon and self.icon.startswith('a_'))
|
||||
|
||||
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the guild's icon.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
|
||||
'gif' is only valid for animated avatars. The size must be a power of 2
|
||||
between 16 and 4096.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the icon to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected into either 'gif' or static_format depending on the
|
||||
icon being animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated icons to.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
|
||||
|
||||
class WebhookAdapter:
|
||||
"""Base class for all webhook adapters.
|
||||
|
||||
@ -65,7 +157,7 @@ class WebhookAdapter:
|
||||
def _prepare(self, webhook):
|
||||
self._webhook_id = webhook.id
|
||||
self._webhook_token = webhook.token
|
||||
self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token)
|
||||
self._request_url = f'{self.BASE}/webhooks/{webhook.id}/{webhook.token}'
|
||||
self.webhook = webhook
|
||||
|
||||
def is_async(self):
|
||||
@ -100,10 +192,10 @@ class WebhookAdapter:
|
||||
return self.request('PATCH', self._request_url, payload=payload, reason=reason)
|
||||
|
||||
def edit_webhook_message(self, message_id, payload):
|
||||
return self.request('PATCH', '{}/messages/{}'.format(self._request_url, message_id), payload=payload)
|
||||
return self.request('PATCH', f'{self._request_url}/messages/{message_id}', payload=payload)
|
||||
|
||||
def delete_webhook_message(self, message_id):
|
||||
return self.request('DELETE', '{}/messages/{}'.format(self._request_url, message_id))
|
||||
return self.request('DELETE', f'{self._request_url}/messages/{message_id}')
|
||||
|
||||
def handle_execution_response(self, data, *, wait):
|
||||
"""Transforms the webhook execution response into something
|
||||
@ -158,7 +250,7 @@ class WebhookAdapter:
|
||||
multipart = None
|
||||
files_to_pass = None
|
||||
|
||||
url = '%s?wait=%d' % (self._request_url, wait)
|
||||
url = f'{self._request_url}?wait={int(wait)}'
|
||||
maybe_coro = None
|
||||
try:
|
||||
maybe_coro = self.request('POST', url, multipart=multipart, payload=data, files=files_to_pass)
|
||||
@ -203,13 +295,6 @@ class AsyncWebhookAdapter(WebhookAdapter):
|
||||
if reason:
|
||||
headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ')
|
||||
|
||||
if multipart:
|
||||
data = aiohttp.FormData()
|
||||
for key, value in multipart.items():
|
||||
if key.startswith('file'):
|
||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
||||
else:
|
||||
data.add_field(key, value)
|
||||
|
||||
base_url = url.replace(self._request_url, '/') or '/'
|
||||
_id = self._webhook_id
|
||||
@ -217,6 +302,14 @@ class AsyncWebhookAdapter(WebhookAdapter):
|
||||
for file in files:
|
||||
file.reset(seek=tries)
|
||||
|
||||
if multipart:
|
||||
data = aiohttp.FormData()
|
||||
for key, value in multipart.items():
|
||||
if key.startswith('file'):
|
||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
||||
else:
|
||||
data.add_field(key, value)
|
||||
|
||||
async with self.session.request(verb, url, headers=headers, data=data) as r:
|
||||
log.debug('Webhook ID %s with %s %s has returned status code %s', _id, verb, base_url, r.status)
|
||||
# Coerce empty strings to return None for hygiene purposes
|
||||
@ -404,10 +497,6 @@ class _PartialWebhookState:
|
||||
def store_user(self, data):
|
||||
return BaseUser(state=self, data=data)
|
||||
|
||||
@property
|
||||
def is_bot(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
if self.parent is not None:
|
||||
@ -421,7 +510,7 @@ class _PartialWebhookState:
|
||||
if self.parent is not None:
|
||||
return getattr(self.parent, attr)
|
||||
|
||||
raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr))
|
||||
raise AttributeError(f'PartialWebhookState does not support {attr!r}.')
|
||||
|
||||
class WebhookMessage(Message):
|
||||
"""Represents a message sent from your webhook.
|
||||
@ -482,7 +571,7 @@ class WebhookMessage(Message):
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(inner_call(), loop=self._state.loop)
|
||||
asyncio.create_task(inner_call())
|
||||
return await asyncio.sleep(0)
|
||||
|
||||
def delete(self, *, delay=None):
|
||||
@ -596,10 +685,21 @@ class Webhook(Hashable):
|
||||
The default name of the webhook.
|
||||
avatar: Optional[:class:`str`]
|
||||
The default avatar of the webhook.
|
||||
source_guild: Optional[:class:`PartialWebhookGuild`]
|
||||
The guild of the channel that this webhook is following.
|
||||
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
source_channel: Optional[:class:`PartialWebhookChannel`]
|
||||
The channel that this webhook is following.
|
||||
Only given if :attr:`type` is :attr:`WebhookType.channel_follower`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'type', 'guild_id', 'channel_id', 'user', 'name',
|
||||
'avatar', 'token', '_state', '_adapter')
|
||||
'avatar', 'token', '_state', '_adapter', 'source_channel', 'source_guild')
|
||||
|
||||
def __init__(self, data, *, adapter, state=None):
|
||||
self.id = int(data['id'])
|
||||
@ -621,13 +721,25 @@ class Webhook(Hashable):
|
||||
else:
|
||||
self.user = User(state=state, data=user)
|
||||
|
||||
source_channel = data.get('source_channel')
|
||||
if source_channel:
|
||||
source_channel = PartialWebhookChannel(data=source_channel)
|
||||
|
||||
self.source_channel = source_channel
|
||||
|
||||
source_guild = data.get('source_guild')
|
||||
if source_guild:
|
||||
source_guild = PartialWebhookGuild(data=source_guild, state=state)
|
||||
|
||||
self.source_guild = source_guild
|
||||
|
||||
def __repr__(self):
|
||||
return '<Webhook id=%r>' % self.id
|
||||
return f'<Webhook id={self.id!r}>'
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`str` : Returns the webhook's url."""
|
||||
return 'https://discord.com/api/webhooks/{}/{}'.format(self.id, self.token)
|
||||
return f'https://discord.com/api/webhooks/{self.id}/{self.token}'
|
||||
|
||||
@classmethod
|
||||
def partial(cls, id, token, *, adapter):
|
||||
@ -687,7 +799,7 @@ class Webhook(Hashable):
|
||||
A partial webhook is just a webhook object with an ID and a token.
|
||||
"""
|
||||
|
||||
m = re.search(r'discord(?:app)?.com/api/webhooks/(?P<id>[0-9]{17,21})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
|
||||
m = re.search(r'discord(?:app)?.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
|
||||
if m is None:
|
||||
raise InvalidArgument('Invalid webhook URL given.')
|
||||
data = m.groupdict()
|
||||
@ -696,7 +808,7 @@ class Webhook(Hashable):
|
||||
|
||||
@classmethod
|
||||
def _as_follower(cls, data, *, channel, user):
|
||||
name = "{} #{}".format(channel.guild, channel)
|
||||
name = f"{channel.guild} #{channel}"
|
||||
feed = {
|
||||
'id': data['webhook_id'],
|
||||
'type': 2,
|
||||
|
@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@ -30,6 +28,12 @@ from .activity import create_activity
|
||||
from .invite import Invite
|
||||
from .enums import Status, try_enum
|
||||
|
||||
__all__ = (
|
||||
'WidgetChannel',
|
||||
'WidgetMember',
|
||||
'Widget',
|
||||
)
|
||||
|
||||
class WidgetChannel:
|
||||
"""Represents a "partial" widget channel.
|
||||
|
||||
@ -77,7 +81,7 @@ class WidgetChannel:
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return '<#%s>' % self.id
|
||||
return f'<#{self.id}>'
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
@ -156,7 +160,7 @@ class WidgetMember(BaseUser):
|
||||
@property
|
||||
def display_name(self):
|
||||
""":class:`str`: Returns the member's display name."""
|
||||
return self.nick if self.nick else self.name
|
||||
return self.nick or self.name
|
||||
|
||||
class Widget:
|
||||
"""Represents a :class:`Guild` widget.
|
||||
@ -236,7 +240,7 @@ class Widget:
|
||||
@property
|
||||
def json_url(self):
|
||||
""":class:`str`: The JSON URL of the widget."""
|
||||
return "https://discord.com/api/guilds/{0.id}/widget.json".format(self)
|
||||
return f"https://discord.com/api/guilds/{self.id}/widget.json"
|
||||
|
||||
@property
|
||||
def invite_url(self):
|
||||
|
430
docs/api.rst
430
docs/api.rst
@ -40,6 +40,10 @@ Client
|
||||
|
||||
.. autoclass:: Client
|
||||
:members:
|
||||
:exclude-members: fetch_guilds
|
||||
|
||||
.. automethod:: Client.fetch_guilds
|
||||
:async-for:
|
||||
|
||||
AutoShardedClient
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
@ -200,11 +204,11 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
|
||||
.. function:: on_disconnect()
|
||||
|
||||
Called when the client has disconnected from Discord. This could happen either through
|
||||
the internet being disconnected, explicit calls to logout, or Discord terminating the connection
|
||||
one way or the other.
|
||||
Called when the client has disconnected from Discord, or a connection attempt to Discord has failed.
|
||||
This could happen either through the internet being disconnected, explicit calls to close,
|
||||
or Discord terminating the connection one way or the other.
|
||||
|
||||
This function can be called many times.
|
||||
This function can be called many times without a corresponding :func:`on_connect` call.
|
||||
|
||||
.. function:: on_shard_disconnect(shard_id)
|
||||
|
||||
@ -250,7 +254,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
:param shard_id: The shard ID that has resumed.
|
||||
:type shard_id: :class:`int`
|
||||
|
||||
.. function:: on_error(event, \*args, \*\*kwargs)
|
||||
.. function:: on_error(event, *args, **kwargs)
|
||||
|
||||
Usually when an event raises an uncaught exception, a traceback is
|
||||
printed to stderr and the exception is ignored. If you want to
|
||||
@ -336,7 +340,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
:type channel: :class:`abc.Messageable`
|
||||
:param user: The user that started typing.
|
||||
: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`
|
||||
|
||||
.. function:: on_message(message)
|
||||
@ -448,7 +452,10 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
regardless of the state of the internal message cache.
|
||||
|
||||
If the message is found in the message cache,
|
||||
it can be accessed via :attr:`RawMessageUpdateEvent.cached_message`
|
||||
it can be accessed via :attr:`RawMessageUpdateEvent.cached_message`. The cached message represents
|
||||
the message before it has been edited. For example, if the content of a message is modified and
|
||||
triggers the :func:`on_raw_message_edit` coroutine, the :attr:`RawMessageUpdateEvent.cached_message`
|
||||
will return a :class:`Message` object that represents the message before the content was modified.
|
||||
|
||||
Due to the inherently raw nature of this event, the data parameter coincides with
|
||||
the raw data given by the `gateway <https://discord.com/developers/docs/topics/gateway#message-update>`_.
|
||||
@ -475,6 +482,14 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
|
||||
This requires :attr:`Intents.reactions` to be enabled.
|
||||
|
||||
.. note::
|
||||
|
||||
This doesn't require :attr:`Intents.members` within a guild context,
|
||||
but due to Discord not providing updated user information in a direct message
|
||||
it's required for direct messages to receive this event.
|
||||
Consider using :func:`on_raw_reaction_add` if you need this and do not otherwise want
|
||||
to enable the members intent.
|
||||
|
||||
:param reaction: The current state of the reaction.
|
||||
:type reaction: :class:`Reaction`
|
||||
:param user: The user who added the reaction.
|
||||
@ -500,7 +515,12 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
|
||||
To get the message being reacted, access it via :attr:`Reaction.message`.
|
||||
|
||||
This requires :attr:`Intents.reactions` to be enabled.
|
||||
This requires both :attr:`Intents.reactions` and :attr:`Intents.members` to be enabled.
|
||||
|
||||
.. note::
|
||||
|
||||
Consider using :func:`on_raw_reaction_remove` if you need this and do not want
|
||||
to enable the members intent.
|
||||
|
||||
:param reaction: The current state of the reaction.
|
||||
:type reaction: :class:`Reaction`
|
||||
@ -565,6 +585,17 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
:param payload: The raw event payload data.
|
||||
: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)
|
||||
on_private_channel_create(channel)
|
||||
|
||||
@ -592,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.
|
||||
: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`]
|
||||
|
||||
.. function:: on_guild_channel_delete(channel)
|
||||
@ -626,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.
|
||||
: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`]
|
||||
|
||||
.. function:: on_guild_integrations_update(guild)
|
||||
@ -877,25 +908,6 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
||||
:param user: The user that joined or left.
|
||||
: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`.
|
||||
|
||||
: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.
|
||||
|
||||
:param before: The previous relationship status.
|
||||
:type before: :class:`Relationship`
|
||||
:param after: The updated relationship status.
|
||||
:type after: :class:`Relationship`
|
||||
|
||||
.. _discord-api-utils:
|
||||
|
||||
Utility Functions
|
||||
@ -909,6 +921,8 @@ Utility Functions
|
||||
|
||||
.. autofunction:: discord.utils.oauth_url
|
||||
|
||||
.. autofunction:: discord.utils.remove_markdown
|
||||
|
||||
.. autofunction:: discord.utils.escape_markdown
|
||||
|
||||
.. autofunction:: discord.utils.escape_mentions
|
||||
@ -919,94 +933,7 @@ Utility Functions
|
||||
|
||||
.. autofunction:: discord.utils.sleep_until
|
||||
|
||||
Profile
|
||||
---------
|
||||
|
||||
.. class:: Profile
|
||||
|
||||
A namedtuple representing a user's Discord public profile.
|
||||
|
||||
.. 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`]]
|
||||
.. autofunction:: discord.utils.utcnow
|
||||
|
||||
.. _discord-api-enums:
|
||||
|
||||
@ -1046,6 +973,12 @@ of :class:`enum.Enum`.
|
||||
|
||||
A guild store channel.
|
||||
|
||||
.. attribute:: stage_voice
|
||||
|
||||
A guild stage voice channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. class:: MessageType
|
||||
|
||||
Specifies the type of :class:`Message`. This is used to denote if a message
|
||||
@ -1108,6 +1041,35 @@ of :class:`enum.Enum`.
|
||||
The system message denoting that an announcement channel has been followed.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
.. attribute:: guild_stream
|
||||
|
||||
The system message denoting that a member is streaming in the guild.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
.. attribute:: guild_discovery_disqualified
|
||||
|
||||
The system message denoting that the guild is no longer eligible for Server
|
||||
Discovery.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
.. attribute:: guild_discovery_requalified
|
||||
|
||||
The system message denoting that the guild has become eligible again for Server
|
||||
Discovery.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
.. attribute:: guild_discovery_grace_period_initial_warning
|
||||
|
||||
The system message denoting that the guild has failed to meet the Server
|
||||
Discovery requirements for one week.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
.. attribute:: guild_discovery_grace_period_final_warning
|
||||
|
||||
The system message denoting that the guild has failed to meet the Server
|
||||
Discovery requirements for 3 weeks in a row.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. class:: ActivityType
|
||||
|
||||
@ -1138,6 +1100,20 @@ of :class:`enum.Enum`.
|
||||
|
||||
.. 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
|
||||
|
||||
Specifies the HypeSquad house a user belongs to.
|
||||
@ -1877,117 +1853,6 @@ of :class:`enum.Enum`.
|
||||
|
||||
The action is the update of something.
|
||||
|
||||
.. class:: RelationshipType
|
||||
|
||||
Specifies the type of :class:`Relationship`.
|
||||
|
||||
.. 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.
|
||||
|
||||
.. 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.
|
||||
|
||||
.. 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.
|
||||
|
||||
.. 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.
|
||||
|
||||
.. 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
|
||||
|
||||
Represents the membership state of a team member retrieved through :func:`Bot.application_info`.
|
||||
@ -2661,20 +2526,19 @@ interface, :meth:`WebhookAdapter.request`.
|
||||
Abstract Base Classes
|
||||
-----------------------
|
||||
|
||||
An :term:`py: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
|
||||
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:`py:isinstance` and :func:`py:issubclass`\.
|
||||
An :term:`abstract base class` (also known as an ``abc``) is a class that models can inherit
|
||||
to get their behaviour. **Abstract base classes should not be instantiated**.
|
||||
They are mainly there for usage with :func:`isinstance` and :func:`issubclass`\.
|
||||
|
||||
This library has a module related to abstract base classes, some of which are actually from the :doc:`abc <py:library/abc>` standard
|
||||
module, others which are not.
|
||||
This library has a module related to abstract base classes, in which all the ABCs are subclasses of
|
||||
:class:`typing.Protocol`.
|
||||
|
||||
Snowflake
|
||||
~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.abc.Snowflake
|
||||
|
||||
.. autoclass:: discord.abc.Snowflake
|
||||
.. autoclass:: discord.abc.Snowflake()
|
||||
:members:
|
||||
|
||||
User
|
||||
@ -2682,7 +2546,7 @@ User
|
||||
|
||||
.. attributetable:: discord.abc.User
|
||||
|
||||
.. autoclass:: discord.abc.User
|
||||
.. autoclass:: discord.abc.User()
|
||||
:members:
|
||||
|
||||
PrivateChannel
|
||||
@ -2690,7 +2554,7 @@ PrivateChannel
|
||||
|
||||
.. attributetable:: discord.abc.PrivateChannel
|
||||
|
||||
.. autoclass:: discord.abc.PrivateChannel
|
||||
.. autoclass:: discord.abc.PrivateChannel()
|
||||
:members:
|
||||
|
||||
GuildChannel
|
||||
@ -2698,7 +2562,7 @@ GuildChannel
|
||||
|
||||
.. attributetable:: discord.abc.GuildChannel
|
||||
|
||||
.. autoclass:: discord.abc.GuildChannel
|
||||
.. autoclass:: discord.abc.GuildChannel()
|
||||
:members:
|
||||
|
||||
Messageable
|
||||
@ -2706,7 +2570,7 @@ Messageable
|
||||
|
||||
.. attributetable:: discord.abc.Messageable
|
||||
|
||||
.. autoclass:: discord.abc.Messageable
|
||||
.. autoclass:: discord.abc.Messageable()
|
||||
:members:
|
||||
:exclude-members: history, typing
|
||||
|
||||
@ -2721,7 +2585,7 @@ Connectable
|
||||
|
||||
.. attributetable:: discord.abc.Connectable
|
||||
|
||||
.. autoclass:: discord.abc.Connectable
|
||||
.. autoclass:: discord.abc.Connectable()
|
||||
|
||||
.. _discord_api_models:
|
||||
|
||||
@ -2759,14 +2623,6 @@ ClientUser
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
Relationship
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: Relationship
|
||||
|
||||
.. autoclass:: Relationship()
|
||||
:members:
|
||||
|
||||
User
|
||||
~~~~~
|
||||
|
||||
@ -2828,22 +2684,6 @@ Reaction
|
||||
.. automethod:: users
|
||||
:async-for:
|
||||
|
||||
CallMessage
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: CallMessage
|
||||
|
||||
.. autoclass:: CallMessage()
|
||||
:members:
|
||||
|
||||
GroupCall
|
||||
~~~~~~~~~~
|
||||
|
||||
.. attributetable:: GroupCall
|
||||
|
||||
.. autoclass:: GroupCall()
|
||||
:members:
|
||||
|
||||
Guild
|
||||
~~~~~~
|
||||
|
||||
@ -2851,11 +2691,30 @@ Guild
|
||||
|
||||
.. autoclass:: Guild()
|
||||
:members:
|
||||
:exclude-members: audit_logs
|
||||
:exclude-members: fetch_members, audit_logs
|
||||
|
||||
.. automethod:: fetch_members
|
||||
:async-for:
|
||||
|
||||
.. automethod:: audit_logs
|
||||
:async-for:
|
||||
|
||||
.. class:: BanEntry
|
||||
|
||||
A namedtuple which represents a ban returned from :meth:`~Guild.bans`.
|
||||
|
||||
.. attribute:: reason
|
||||
|
||||
The reason this user was banned.
|
||||
|
||||
:type: Optional[:class:`str`]
|
||||
.. attribute:: user
|
||||
|
||||
The :class:`User` that was banned.
|
||||
|
||||
:type: :class:`User`
|
||||
|
||||
|
||||
Integration
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@ -2865,6 +2724,14 @@ Integration
|
||||
.. autoclass:: IntegrationAccount()
|
||||
:members:
|
||||
|
||||
Interaction
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: Interaction
|
||||
|
||||
.. autoclass:: Interaction()
|
||||
:members:
|
||||
|
||||
Member
|
||||
~~~~~~
|
||||
|
||||
@ -2943,6 +2810,15 @@ TextChannel
|
||||
.. automethod:: typing
|
||||
:async-with:
|
||||
|
||||
StoreChannel
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: StoreChannel
|
||||
|
||||
.. autoclass:: StoreChannel()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
VoiceChannel
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@ -2952,6 +2828,15 @@ VoiceChannel
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
StageChannel
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: StageChannel
|
||||
|
||||
.. autoclass:: StageChannel()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
CategoryChannel
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -3106,6 +2991,21 @@ RawReactionClearEmojiEvent
|
||||
.. autoclass:: RawReactionClearEmojiEvent()
|
||||
:members:
|
||||
|
||||
PartialWebhookGuild
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: PartialWebhookGuild
|
||||
|
||||
.. autoclass:: PartialWebhookGuild()
|
||||
:members:
|
||||
|
||||
PartialWebhookChannel
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: PartialWebhookChannel
|
||||
|
||||
.. autoclass:: PartialWebhookChannel()
|
||||
:members:
|
||||
|
||||
.. _discord_api_data:
|
||||
|
||||
|
20
docs/conf.py
20
docs/conf.py
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# discord.py documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Aug 21 05:43:30 2015.
|
||||
@ -44,6 +43,7 @@ extensions = [
|
||||
]
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_typehints = 'none'
|
||||
|
||||
extlinks = {
|
||||
'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-'),
|
||||
@ -76,8 +76,8 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'discord.py'
|
||||
copyright = u'2015-2021, Rapptz'
|
||||
project = 'discord.py'
|
||||
copyright = '2015-present, Rapptz'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@ -160,7 +160,7 @@ resource_links = {
|
||||
'discord': 'https://discord.gg/r3sSKJJ',
|
||||
'issues': 'https://github.com/Rapptz/discord.py/issues',
|
||||
'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
|
||||
@ -283,8 +283,8 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'discord.py.tex', u'discord.py Documentation',
|
||||
u'Rapptz', 'manual'),
|
||||
('index', 'discord.py.tex', 'discord.py Documentation',
|
||||
'Rapptz', 'manual'),
|
||||
]
|
||||
|
||||
# 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
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'discord.py', u'discord.py Documentation',
|
||||
[u'Rapptz'], 1)
|
||||
('index', 'discord.py', 'discord.py Documentation',
|
||||
['Rapptz'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@ -327,8 +327,8 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'discord.py', u'discord.py Documentation',
|
||||
u'Rapptz', 'discord.py', 'One line description of project.',
|
||||
('index', 'discord.py', 'discord.py Documentation',
|
||||
'Rapptz', 'discord.py', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
@ -281,18 +281,27 @@ Converters
|
||||
.. autoclass:: discord.ext.commands.MessageConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.PartialMessageConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.TextChannelConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.VoiceChannelConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.StoreChannelConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.CategoryChannelConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.InviteConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.GuildConverter
|
||||
:members:
|
||||
|
||||
.. autoclass:: discord.ext.commands.RoleConverter
|
||||
:members:
|
||||
|
||||
@ -407,6 +416,9 @@ Exceptions
|
||||
.. autoexception:: discord.ext.commands.MemberNotFound
|
||||
:members:
|
||||
|
||||
.. autoexception:: discord.ext.commands.GuildNotFound
|
||||
:members:
|
||||
|
||||
.. autoexception:: discord.ext.commands.UserNotFound
|
||||
:members:
|
||||
|
||||
|
@ -193,6 +193,8 @@ Converters come in a few flavours:
|
||||
|
||||
- A custom class that inherits from :class:`~ext.commands.Converter`.
|
||||
|
||||
.. _ext_commands_basic_converters:
|
||||
|
||||
Basic Converters
|
||||
++++++++++++++++++
|
||||
|
||||
@ -373,16 +375,19 @@ A lot of discord models work out of the gate as a parameter:
|
||||
|
||||
- :class:`Member`
|
||||
- :class:`User`
|
||||
- :class:`Message` (since v1.1)
|
||||
- :class:`PartialMessage` (since v1.7)
|
||||
- :class:`TextChannel`
|
||||
- :class:`VoiceChannel`
|
||||
- :class:`StoreChannel` (since v1.7)
|
||||
- :class:`CategoryChannel`
|
||||
- :class:`Role`
|
||||
- :class:`Message` (since v1.1)
|
||||
- :class:`Invite`
|
||||
- :class:`Guild` (since v1.7)
|
||||
- :class:`Role`
|
||||
- :class:`Game`
|
||||
- :class:`Colour`
|
||||
- :class:`Emoji`
|
||||
- :class:`PartialEmoji`
|
||||
- :class:`Colour`
|
||||
|
||||
Having any of these set as the converter will intelligently convert the argument to the appropriate target type you
|
||||
specify.
|
||||
@ -395,28 +400,34 @@ converter is given below:
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Member` | :class:`~ext.commands.MemberConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`User` | :class:`~ext.commands.UserConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Message` | :class:`~ext.commands.MessageConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`User` | :class:`~ext.commands.UserConverter` |
|
||||
| :class:`PartialMessage` | :class:`~ext.commands.PartialMessageConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
|
||||
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Role` | :class:`~ext.commands.RoleConverter` |
|
||||
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Invite` | :class:`~ext.commands.InviteConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Guild` | :class:`~ext.commands.GuildConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Role` | :class:`~ext.commands.RoleConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Game` | :class:`~ext.commands.GameConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Colour` | :class:`~ext.commands.ColourConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Emoji` | :class:`~ext.commands.EmojiConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`PartialEmoji` | :class:`~ext.commands.PartialEmojiConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
| :class:`Colour` | :class:`~ext.commands.ColourConverter` |
|
||||
+--------------------------+-------------------------------------------------+
|
||||
|
||||
By providing the converter it allows us to use them as building blocks for another converter:
|
||||
|
||||
|
@ -139,5 +139,6 @@ API Reference
|
||||
|
||||
.. autoclass:: discord.ext.tasks.Loop()
|
||||
:members:
|
||||
:special-members: __call__
|
||||
|
||||
.. autofunction:: discord.ext.tasks.loop
|
||||
|
@ -58,11 +58,13 @@ def add_custom_jinja2(app):
|
||||
|
||||
def add_builders(app):
|
||||
"""This is necessary because RTD injects their own for some reason."""
|
||||
app.set_translator('html', DPYHTML5Translator, override=True)
|
||||
app.add_builder(DPYStandaloneHTMLBuilder, override=True)
|
||||
|
||||
try:
|
||||
original = app.registry.builders['readthedocs']
|
||||
except KeyError:
|
||||
app.set_translator('html', DPYHTML5Translator, override=True)
|
||||
app.add_builder(DPYStandaloneHTMLBuilder, override=True)
|
||||
pass
|
||||
else:
|
||||
injected_mro = tuple(base if base is not StandaloneHTMLBuilder else DPYStandaloneHTMLBuilder
|
||||
for base in original.mro()[1:])
|
||||
|
15
docs/faq.rst
15
docs/faq.rst
@ -85,8 +85,15 @@ in the repository.
|
||||
How do I set the "Playing" status?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There is a method for this under :class:`Client` called :meth:`Client.change_presence`.
|
||||
The relevant aspect of this is its ``activity`` keyword argument which takes in an :class:`Activity` object.
|
||||
The ``activity`` keyword argument may be passed in the :class:`Client` constructor or :meth:`Client.change_presence`, given an :class:`Activity` object.
|
||||
|
||||
The constructor may be used for static activities, while :meth:`Client.change_presence` may be used to update the activity at runtime.
|
||||
|
||||
.. warning::
|
||||
|
||||
It is highly discouraged to use :meth:`Client.change_presence` or API calls in :func:`on_ready` as this event may be called many times while running, not just once.
|
||||
|
||||
There is a high chance of disconnecting if presences are changed right after connecting.
|
||||
|
||||
The status type (playing, listening, streaming, watching) can be set using the :class:`ActivityType` enum.
|
||||
For memory optimisation purposes, some activities are offered in slimmed down versions:
|
||||
@ -96,11 +103,11 @@ For memory optimisation purposes, some activities are offered in slimmed down ve
|
||||
|
||||
Putting both of these pieces of info together, you get the following: ::
|
||||
|
||||
await client.change_presence(activity=discord.Game(name='my game'))
|
||||
client = discord.Client(activity=discord.Game(name='my game'))
|
||||
|
||||
# or, for watching:
|
||||
activity = discord.Activity(name='my activity', type=discord.ActivityType.watching)
|
||||
await client.change_presence(activity=activity)
|
||||
client = discord.Client(activity=activity)
|
||||
|
||||
How do I send a message to a specific channel?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -5,7 +5,7 @@
|
||||
A Primer to Gateway Intents
|
||||
=============================
|
||||
|
||||
In version 1.5 comes the introduction of :class:`Intents`. This is a radical change in how bots are written. An intent basically allows a bot to subscribe into specific buckets of events. The events that correspond to each intent is documented in the individual attribute of the :class:`Intents` documentation.
|
||||
In version 1.5 comes the introduction of :class:`Intents`. This is a radical change in how bots are written. An intent basically allows a bot to subscribe to specific buckets of events. The events that correspond to each intent is documented in the individual attribute of the :class:`Intents` documentation.
|
||||
|
||||
These intents are passed to the constructor of :class:`Client` or its subclasses (:class:`AutoShardedClient`, :class:`~.AutoShardedBot`, :class:`~.Bot`) with the ``intents`` argument.
|
||||
|
||||
@ -118,9 +118,10 @@ It should be noted that certain things do not need a member cache since Discord
|
||||
|
||||
- :func:`on_message` will have :attr:`Message.author` be a member even if cache is disabled.
|
||||
- :func:`on_voice_state_update` will have the ``member`` parameter be a member even if cache is disabled.
|
||||
- :func:`on_reaction_add` will have the ``user`` parameter be a member even if cache is disabled.
|
||||
- :func:`on_raw_reaction_add` will have :attr:`RawReactionActionEvent.member` be a member even if cache is disabled.
|
||||
- The reaction removal events do not have the member information. This is a Discord limitation.
|
||||
- :func:`on_reaction_add` will have the ``user`` parameter be a member when in a guild even if cache is disabled.
|
||||
- :func:`on_raw_reaction_add` will have :attr:`RawReactionActionEvent.member` be a member when in a guild even if cache is disabled.
|
||||
- The reaction add events do not contain additional information when in direct messages. This is a Discord limitation.
|
||||
- The reaction removal events do not have member information. This is a Discord limitation.
|
||||
|
||||
Other events that take a :class:`Member` will require the use of the member cache. If absolute accuracy over the member cache is desirable, then it is advisable to have the :attr:`Intents.members` intent enabled.
|
||||
|
||||
@ -129,7 +130,7 @@ Other events that take a :class:`Member` will require the use of the member cach
|
||||
Retrieving Members
|
||||
--------------------
|
||||
|
||||
If cache is disabled or you disable chunking guilds at startup, we might still need a way to load members. The library offers a few ways to do this:
|
||||
If the cache is disabled or you disable chunking guilds at startup, we might still need a way to load members. The library offers a few ways to do this:
|
||||
|
||||
- :meth:`Guild.query_members`
|
||||
- Used to query members by a prefix matching nickname or username.
|
||||
@ -180,27 +181,10 @@ The first solution is to request the privileged presences intent along with the
|
||||
|
||||
The second solution is to disable member chunking by setting ``chunk_guilds_at_startup`` to ``False`` when constructing a client. Then, when chunking for a guild is necessary you can use the various techniques to :ref:`retrieve members <retrieving_members>`.
|
||||
|
||||
To illustrate the slowdown caused the API change, take a bot who is in 840 guilds and 95 of these guilds are "large" (over 250 members).
|
||||
To illustrate the slowdown caused by the API change, take a bot who is in 840 guilds and 95 of these guilds are "large" (over 250 members).
|
||||
|
||||
Under the original system this would result in 2 requests to fetch the member list (75 guilds, 20 guilds) roughly taking 60 seconds. With :attr:`Intents.members` but not :attr:`Intents.presences` this requires 840 requests, with a rate limit of 120 requests per 60 seconds means that due to waiting for the rate limit it totals to around 7 minutes of waiting for the rate limit to fetch all the members. With both :attr:`Intents.members` and :attr:`Intents.presences` we mostly get the old behaviour so we're only required to request for the 95 guilds that are large, this is slightly less than our rate limit so it's close to the original timing to fetch the member list.
|
||||
|
||||
Unfortunately due to this change being required from Discord there is nothing that the library can do to mitigate this.
|
||||
|
||||
I don't like this, can I go back?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For now, the old gateway will still work so downgrading to discord.py v1.4 is still possible and will continue to be supported until Discord officially kills the v6 gateway, which is imminent. However it is paramount that for the future of your bot that you upgrade your code to the new way things are done.
|
||||
|
||||
To downgrade you can do the following:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
python3 -m pip install -U "discord.py>=1.4,<1.5"
|
||||
|
||||
On Windows use ``py -3`` instead of ``python3``.
|
||||
|
||||
.. warning::
|
||||
|
||||
There is no currently set date in which the old gateway will stop working so it is recommended to update your code instead.
|
||||
|
||||
If you truly dislike the direction Discord is going with their API, you can contact them via `support <https://dis.gd/contact>`_.
|
||||
|
@ -1,5 +1,5 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2015-2020, Rapptz
|
||||
# Copyright (C) 2015-present, Rapptz
|
||||
# This file is distributed under the same license as the discord.py package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
|
||||
#
|
||||
@ -427,4 +427,3 @@ msgid ""
|
||||
"If you truly dislike the direction Discord is going with their API, you "
|
||||
"can contact them via `support <https://dis.gd/contact>`_"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11,6 +11,97 @@ Changelog
|
||||
This page keeps a detailed human friendly rendering of what's new and changed
|
||||
in specific versions.
|
||||
|
||||
.. _vp1p7p1:
|
||||
|
||||
v1.7.1
|
||||
-------
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~~~
|
||||
|
||||
- |commands| Fix :meth:`Cog.has_error_handler <ext.commands.Cog.has_error_handler>` not working as intended.
|
||||
|
||||
.. _vp1p7p0:
|
||||
|
||||
v1.7.0
|
||||
--------
|
||||
|
||||
This version is mainly for improvements and bug fixes. This is more than likely the last major version in the 1.x series.
|
||||
Work after this will be spent on v2.0. As a result, **this is the last version to support Python 3.5**.
|
||||
Likewise, **this is the last version to support user bots**.
|
||||
|
||||
Development of v2.0 will have breaking changes and support for newer API features.
|
||||
|
||||
|
||||
- Add support for stage channels via :class:`StageChannel` (:issue:`6602`, :issue:`6608`)
|
||||
- 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.
|
||||
This attribute tells the Discord API that it's okay for that message to be missing.
|
||||
|
||||
- Add support for Discord's new permission serialisation scheme.
|
||||
- Add an easier way to move channels using :meth:`abc.GuildChannel.move`
|
||||
- Add :attr:`Permissions.use_slash_commands`
|
||||
- Add :attr:`Permissions.request_to_speak`
|
||||
- Add support for voice regions in voice channels via :attr:`VoiceChannel.rtc_region` (:issue:`6606`)
|
||||
- Add support for :meth:`PartialEmoji.url_as` (:issue:`6341`)
|
||||
- Add :attr:`MessageReference.jump_url` (:issue:`6318`)
|
||||
- Add :attr:`File.spoiler` (:issue:`6317`)
|
||||
- Add support for passing ``roles`` to :meth:`Guild.estimate_pruned_members` (:issue:`6538`)
|
||||
- Allow callable class factories to be used in :meth:`abc.Connectable.play` (:issue:`6478`)
|
||||
- Add a way to get mutual guilds from the client's cache via :attr:`User.mutual_guilds` (:issue:`2539`, :issue:`6444`)
|
||||
- :meth:`PartialMessage.edit` now returns a full :class:`Message` upon success (:issue:`6309`)
|
||||
- Add :attr:`RawMessageUpdateEvent.guild_id` (:issue:`6489`)
|
||||
- :class:`AuditLogEntry` is now hashable (:issue:`6495`)
|
||||
- :class:`Attachment` is now hashable
|
||||
- Add :attr:`Attachment.content_type` attribute (:issue:`6618`)
|
||||
- Add support for casting :class:`Attachment` to :class:`str` to get the URL.
|
||||
- Add ``seed`` parameter for :class:`Colour.random` (:issue:`6562`)
|
||||
- This only seeds it for one call. If seeding for multiple calls is desirable, use :func:`random.seed`.
|
||||
|
||||
- Add a :func:`utils.remove_markdown` helper function (:issue:`6573`)
|
||||
- Add support for passing scopes to :func:`utils.oauth_url` (:issue:`6568`)
|
||||
- |commands| Add support for ``rgb`` CSS function as a parameter to :class:`ColourConverter <ext.commands.ColourConverter>` (:issue:`6374`)
|
||||
- |commands| Add support for converting :class:`StoreChannel` via :class:`StoreChannelConverter <ext.commands.StoreChannelConverter>` (:issue:`6603`)
|
||||
- |commands| Add support for stripping whitespace after the prefix is encountered using the ``strip_after_prefix`` :class:`~ext.commands.Bot` constructor parameter.
|
||||
- |commands| Add :attr:`Context.invoked_parents <ext.commands.Context.invoked_parents>` to get the aliases a command's parent was invoked with (:issue:`1874`, :issue:`6462`)
|
||||
- |commands| Add a converter for :class:`PartialMessage` under :class:`ext.commands.PartialMessageConverter` (:issue:`6308`)
|
||||
- |commands| Add a converter for :class:`Guild` under :class:`ext.commands.GuildConverter` (:issue:`6016`, :issue:`6365`)
|
||||
- |commands| Add :meth:`Command.has_error_handler <ext.commands.Command.has_error_handler>`
|
||||
- This is also adds :meth:`Cog.has_error_handler <ext.commands.Cog.has_error_handler>`
|
||||
- |commands| Allow callable types to act as a bucket key for cooldowns (:issue:`6563`)
|
||||
- |commands| Add ``linesep`` keyword argument to :class:`Paginator <ext.commands.Paginator>` (:issue:`5975`)
|
||||
- |commands| Allow ``None`` to be passed to :attr:`HelpCommand.verify_checks <ext.commands.HelpCommand.verify_checks>` to only verify in a guild context (:issue:`2008`, :issue:`6446`)
|
||||
- |commands| Allow relative paths when loading extensions via a ``package`` keyword argument (:issue:`2465`, :issue:`6445`)
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~~
|
||||
|
||||
- Fix mentions not working if ``mention_author`` is passed in :meth:`abc.Messageable.send` without :attr:`Client.allowed_mentions` set (:issue:`6192`, :issue:`6458`)
|
||||
- Fix user created instances of :class:`CustomActivity` triggering an error (:issue:`4049`)
|
||||
- Note that currently, bot users still cannot set a custom activity due to a Discord limitation.
|
||||
- Fix :exc:`ZeroDivisionError` being raised from :attr:`VoiceClient.average_latency` (:issue:`6430`, :issue:`6436`)
|
||||
- Fix :attr:`User.public_flags` not updating upon edit (:issue:`6315`)
|
||||
- Fix :attr:`Message.call` sometimes causing attribute errors (:issue:`6390`)
|
||||
- Fix issue resending a file during request retries on newer versions of ``aiohttp`` (:issue:`6531`)
|
||||
- Raise an error when ``user_ids`` is empty in :meth:`Guild.query_members`
|
||||
- Fix ``__str__`` magic method raising when a :class:`Guild` is unavailable.
|
||||
- Fix potential :exc:`AttributeError` when accessing :attr:`VoiceChannel.members` (:issue:`6602`)
|
||||
- :class:`Embed` constructor parameters now implicitly convert to :class:`str` (:issue:`6574`)
|
||||
- Ensure ``discord`` package is only run if executed as a script (:issue:`6483`)
|
||||
- |commands| Fix irrelevant commands potentially being unloaded during cog unload due to failure.
|
||||
- |commands| Fix attribute errors when setting a cog to :class:`~.ext.commands.HelpCommand` (:issue:`5154`)
|
||||
- |commands| Fix :attr:`Context.invoked_with <ext.commands.Context.invoked_with>` being improperly reassigned during a :meth:`~ext.commands.Context.reinvoke` (:issue:`6451`, :issue:`6462`)
|
||||
- |commands| Remove duplicates from :meth:`HelpCommand.get_bot_mapping <ext.commands.HelpCommand.get_bot_mapping>` (:issue:`6316`)
|
||||
- |commands| Properly handle positional-only parameters in bot command signatures (:issue:`6431`)
|
||||
- |commands| Group signatures now properly show up in :attr:`Command.signature <ext.commands.Command.signature>` (:issue:`6529`, :issue:`6530`)
|
||||
|
||||
Miscellaneous
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- User endpoints and all userbot related functionality has been deprecated and will be removed in the next major version of the library.
|
||||
- :class:`Permission` class methods were updated to match the UI of the Discord client (:issue:`6476`)
|
||||
- ``_`` and ``-`` characters are now stripped when making a new cog using the ``discord`` package (:issue:`6313`)
|
||||
|
||||
.. _vp1p6p0p7:
|
||||
|
||||
v1.6.0.7
|
||||
@ -24,49 +115,6 @@ New Features
|
||||
- Add ``silent`` kwargs to :meth:`Message.delete`
|
||||
- Add :meth:`Client.get_message`
|
||||
|
||||
.. _vp1p5p1p6:
|
||||
|
||||
v1.5.1.6
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- Add :meth:`Colour.random`
|
||||
|
||||
.. _vp1p5p1p5:
|
||||
|
||||
v1.5.1.5
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- Add :meth:`Colour.nitro_booster`
|
||||
- Add :attr:`Permissions.admin` as alias to :attr:`Permissions.administrator`
|
||||
|
||||
New Beta Features
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These are all for message replies. I have added them to 1.5.1.5 but they will most likely officially get added in the original lib in 1.6 or 2.0
|
||||
|
||||
- |commands| Add :meth:`Context.reply <ext.commands.Context>`
|
||||
- Add :meth:`Message.reply`
|
||||
- Add ``replied_user`` to :class:`AllowedMentions`
|
||||
- Add :meth:`MessageReference.to_dict`
|
||||
- Add :meth:`MessageReference.from_message`
|
||||
- Add ``message_reference`` kwarg to :meth:`abc.Messageable.send`
|
||||
|
||||
.. _vp1p5p1p4:
|
||||
|
||||
v1.5.1.4
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- |commands| Add :attr:`Context.clean_prefix <ext.commands.Context>`
|
||||
|
||||
.. _vp1p6p0:
|
||||
|
||||
v1.6.0
|
||||
@ -139,6 +187,49 @@ Miscellaneous
|
||||
- |commands| :class:`UserConverter <ext.commands.UserConverter>` now fetches the API if an ID is passed and the user is not cached.
|
||||
- |commands| :func:`max_concurrency <ext.commands.max_concurrency>` is now called before cooldowns (:issue:`6172`)
|
||||
|
||||
.. _vp1p5p1p6:
|
||||
|
||||
v1.5.1.6
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- Add :meth:`Colour.random`
|
||||
|
||||
.. _vp1p5p1p5:
|
||||
|
||||
v1.5.1.5
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- Add :meth:`Colour.nitro_booster`
|
||||
- Add :attr:`Permissions.admin` as alias to :attr:`Permissions.administrator`
|
||||
|
||||
New Beta Features
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These are all for message replies. I have added them to 1.5.1.5 but they will most likely officially get added in the original lib in 1.6 or 2.0
|
||||
|
||||
- |commands| Add :meth:`Context.reply <ext.commands.Context>`
|
||||
- Add :meth:`Message.reply`
|
||||
- Add ``replied_user`` to :class:`AllowedMentions`
|
||||
- Add :meth:`MessageReference.to_dict`
|
||||
- Add :meth:`MessageReference.from_message`
|
||||
- Add ``message_reference`` kwarg to :meth:`abc.Messageable.send`
|
||||
|
||||
.. _vp1p5p1p4:
|
||||
|
||||
v1.5.1.4
|
||||
--------
|
||||
|
||||
New Features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- |commands| Add :attr:`Context.clean_prefix <ext.commands.Context>`
|
||||
|
||||
.. _vp1p5p1:
|
||||
|
||||
v1.5.1
|
||||
|
@ -1,12 +1,16 @@
|
||||
from discord.ext import tasks
|
||||
|
||||
import discord
|
||||
import asyncio
|
||||
|
||||
class MyClient(discord.Client):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# create the background task and run it in the background
|
||||
self.bg_task = self.loop.create_task(self.my_background_task())
|
||||
# an attribute we can access from our task
|
||||
self.counter = 0
|
||||
|
||||
# start the task to run in the background
|
||||
self.my_background_task.start()
|
||||
|
||||
async def on_ready(self):
|
||||
print('Logged in as')
|
||||
@ -14,15 +18,15 @@ class MyClient(discord.Client):
|
||||
print(self.user.id)
|
||||
print('------')
|
||||
|
||||
@tasks.loop(seconds=60) # task runs every 60 seconds
|
||||
async def my_background_task(self):
|
||||
await self.wait_until_ready()
|
||||
counter = 0
|
||||
channel = self.get_channel(1234567) # channel ID goes here
|
||||
while not self.is_closed():
|
||||
counter += 1
|
||||
await channel.send(counter)
|
||||
await asyncio.sleep(60) # task runs every 60 seconds
|
||||
self.counter += 1
|
||||
await channel.send(self.counter)
|
||||
|
||||
@my_background_task.before_loop
|
||||
async def before_my_task(self):
|
||||
await self.wait_until_ready() # wait until the bot logs in
|
||||
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
28
examples/background_task_asyncio.py
Normal file
28
examples/background_task_asyncio.py
Normal file
@ -0,0 +1,28 @@
|
||||
import discord
|
||||
import asyncio
|
||||
|
||||
class MyClient(discord.Client):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# create the background task and run it in the background
|
||||
self.bg_task = self.loop.create_task(self.my_background_task())
|
||||
|
||||
async def on_ready(self):
|
||||
print('Logged in as')
|
||||
print(self.user.name)
|
||||
print(self.user.id)
|
||||
print('------')
|
||||
|
||||
async def my_background_task(self):
|
||||
await self.wait_until_ready()
|
||||
counter = 0
|
||||
channel = self.get_channel(1234567) # channel ID goes here
|
||||
while not self.is_closed():
|
||||
counter += 1
|
||||
await channel.send(counter)
|
||||
await asyncio.sleep(60) # task runs every 60 seconds
|
||||
|
||||
|
||||
client = MyClient()
|
||||
client.run('token')
|
@ -61,7 +61,7 @@ async def cool(ctx):
|
||||
In reality this just checks if a subcommand is being invoked.
|
||||
"""
|
||||
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')
|
||||
async def _bot(ctx):
|
||||
|
@ -70,9 +70,9 @@ class Music(commands.Cog):
|
||||
"""Plays a file from the local filesystem"""
|
||||
|
||||
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()
|
||||
async def yt(self, ctx, *, url):
|
||||
@ -80,9 +80,9 @@ class Music(commands.Cog):
|
||||
|
||||
async with ctx.typing():
|
||||
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()
|
||||
async def stream(self, ctx, *, url):
|
||||
@ -90,9 +90,9 @@ class Music(commands.Cog):
|
||||
|
||||
async with ctx.typing():
|
||||
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()
|
||||
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.")
|
||||
|
||||
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()
|
||||
async def stop(self, ctx):
|
||||
|
113
examples/converters.py
Normal file
113
examples/converters.py
Normal file
@ -0,0 +1,113 @@
|
||||
# This example requires the 'members' privileged intent to use the Member converter.
|
||||
|
||||
import typing
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
bot = commands.Bot('!', intents=intents)
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def userinfo(ctx: commands.Context, user: discord.User):
|
||||
# In the command signature above, you can see that the `user`
|
||||
# parameter is typehinted to `discord.User`. This means that
|
||||
# during command invocation we will attempt to convert
|
||||
# the value passed as `user` to a `discord.User` instance.
|
||||
# The documentation notes what can be converted, in the case of `discord.User`
|
||||
# you pass an ID, mention or username (discrim optional)
|
||||
# E.g. 80088516616269824, @Danny or Danny#0007
|
||||
|
||||
# NOTE: typehinting acts as a converter within the `commands` framework only.
|
||||
# In standard Python, it is use for documentation and IDE assistance purposes.
|
||||
|
||||
# If the conversion is successful, we will have a `discord.User` instance
|
||||
# and can do the following:
|
||||
user_id = user.id
|
||||
username = user.name
|
||||
avatar = user.avatar_url
|
||||
await ctx.send(f'User found: {user_id} -- {username}\n{avatar}')
|
||||
|
||||
@userinfo.error
|
||||
async def userinfo_error(ctx: commands.Context, error: commands.CommandError):
|
||||
# if the conversion above fails for any reason, it will raise `commands.BadArgument`
|
||||
# so we handle this in this error handler:
|
||||
if isinstance(error, commands.BadArgument):
|
||||
return await ctx.send('Couldn\'t find that user.')
|
||||
|
||||
# Custom Converter here
|
||||
class ChannelOrMemberConverter(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, argument: str):
|
||||
# In this example we have made a custom converter.
|
||||
# This checks if an input is convertible to a
|
||||
# `discord.Member` or `discord.TextChannel` instance from the
|
||||
# input the user has given us using the pre-existing converters
|
||||
# that the library provides.
|
||||
|
||||
member_converter = commands.MemberConverter()
|
||||
try:
|
||||
# Try and convert to a Member instance.
|
||||
# If this fails, then an exception is raised.
|
||||
# Otherwise, we just return the converted member value.
|
||||
member = await member_converter.convert(ctx, argument)
|
||||
except commands.MemberNotFound:
|
||||
pass
|
||||
else:
|
||||
return member
|
||||
|
||||
# Do the same for TextChannel...
|
||||
textchannel_converter = commands.TextChannelConverter()
|
||||
try:
|
||||
channel = await textchannel_converter.convert(ctx, argument)
|
||||
except commands.ChannelNotFound:
|
||||
pass
|
||||
else:
|
||||
return channel
|
||||
|
||||
# If the value could not be converted we can raise an error
|
||||
# so our error handlers can deal with it in one place.
|
||||
# The error has to be CommandError derived, so BadArgument works fine here.
|
||||
raise commands.BadArgument(f'No Member or TextChannel could be converted from "{argument}"')
|
||||
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def notify(ctx: commands.Context, target: ChannelOrMemberConverter):
|
||||
# This command signature utilises the custom converter written above
|
||||
# What will happen during command invocation is that the `target` above will be passed to
|
||||
# the `argument` parameter of the `ChannelOrMemberConverter.convert` method and
|
||||
# the conversion will go through the process defined there.
|
||||
|
||||
await target.send(f'Hello, {target.name}!')
|
||||
|
||||
@bot.command()
|
||||
async def ignore(ctx: commands.Context, target: typing.Union[discord.Member, discord.TextChannel]):
|
||||
# This command signature utilises the `typing.Union` typehint.
|
||||
# The `commands` framework attempts a conversion of each type in this Union *in order*.
|
||||
# So, it will attempt to convert whatever is passed to `target` to a `discord.Member` instance.
|
||||
# If that fails, it will attempt to convert it to a `discord.TextChannel` instance.
|
||||
# See: https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#typing-union
|
||||
# NOTE: If a Union typehint converter fails it will raise `commands.BadUnionArgument`
|
||||
# instead of `commands.BadArgument`.
|
||||
|
||||
# To check the resulting type, `isinstance` is used
|
||||
if isinstance(target, discord.Member):
|
||||
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.
|
||||
await ctx.send(f'Channel found: {target.mention}, adding it to the ignore list.')
|
||||
|
||||
# Built-in type converters.
|
||||
@bot.command()
|
||||
async def multiply(ctx: commands.Context, number: int, maybe: bool):
|
||||
# We want an `int` and a `bool` parameter here.
|
||||
# `bool` is a slightly special case, as shown here:
|
||||
# See: https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#bool
|
||||
|
||||
if maybe is True:
|
||||
return await ctx.send(number * 2)
|
||||
await ctx.send(number * 5)
|
||||
|
||||
bot.run('token')
|
@ -25,12 +25,12 @@ class MyClient(discord.Client):
|
||||
try:
|
||||
guess = await self.wait_for('message', check=is_correct, timeout=5.0)
|
||||
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:
|
||||
await message.channel.send('You are right!')
|
||||
else:
|
||||
await message.channel.send('Oops. It is actually {}.'.format(answer))
|
||||
await message.channel.send(f'Oops. It is actually {answer}.')
|
||||
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -12,7 +12,7 @@ class MyClient(discord.Client):
|
||||
async def on_member_join(self, member):
|
||||
guild = member.guild
|
||||
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)
|
||||
|
||||
|
||||
|
@ -1,83 +1,85 @@
|
||||
"""Uses a messages to add and remove roles through reactions."""
|
||||
# This example requires the 'members' privileged intents
|
||||
|
||||
import discord
|
||||
|
||||
class RoleReactClient(discord.Client):
|
||||
class MyClient(discord.Client):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.role_message_id = 0 # ID of message that can be reacted to to add role
|
||||
self.role_message_id = 0 # ID of the message that can be reacted to to add/remove a role.
|
||||
self.emoji_to_role = {
|
||||
partial_emoji_1: 0, # ID of role associated with partial emoji object 'partial_emoji_1'
|
||||
partial_emoji_2: 0 # ID of role associated with partial emoji object 'partial_emoji_2'
|
||||
discord.PartialEmoji(name='🔴'): 0, # ID of the role associated with unicode emoji '🔴'.
|
||||
discord.PartialEmoji(name='🟡'): 0, # ID of the role associated with unicode emoji '🟡'.
|
||||
discord.PartialEmoji(name='green', id=0): 0, # ID of the role associated with a partial emoji's ID.
|
||||
}
|
||||
|
||||
async def on_raw_reaction_add(self, payload):
|
||||
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
|
||||
"""Gives a role based on a reaction emoji."""
|
||||
# Make sure that the message the user is reacting to is the one we care about
|
||||
# Make sure that the message the user is reacting to is the one we care about.
|
||||
if payload.message_id != self.role_message_id:
|
||||
return
|
||||
|
||||
guild = self.get_guild(payload.guild_id)
|
||||
if guild is None:
|
||||
# Check if we're still in the guild and it's cached.
|
||||
return
|
||||
|
||||
try:
|
||||
role_id = self.emoji_to_role[payload.emoji]
|
||||
except KeyError:
|
||||
# If the emoji isn't the one we care about then exit as well.
|
||||
return
|
||||
|
||||
guild = self.get_guild(payload.guild_id)
|
||||
if guild is None:
|
||||
# Check if we're still in the guild and it's cached.
|
||||
return
|
||||
|
||||
role = guild.get_role(role_id)
|
||||
if role is None:
|
||||
# Make sure the role still exists and is valid.
|
||||
return
|
||||
|
||||
try:
|
||||
# Finally add the role
|
||||
# Finally, add the role.
|
||||
await payload.member.add_roles(role)
|
||||
except discord.HTTPException:
|
||||
# If we want to do something in case of errors we'd do it here.
|
||||
pass
|
||||
|
||||
async def on_raw_reaction_remove(self, payload):
|
||||
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
|
||||
"""Removes a role based on a reaction emoji."""
|
||||
# Make sure that the message the user is reacting to is the one we care about
|
||||
# Make sure that the message the user is reacting to is the one we care about.
|
||||
if payload.message_id != self.role_message_id:
|
||||
return
|
||||
|
||||
guild = self.get_guild(payload.guild_id)
|
||||
if guild is None:
|
||||
# Check if we're still in the guild and it's cached.
|
||||
return
|
||||
|
||||
try:
|
||||
role_id = self.emoji_to_role[payload.emoji]
|
||||
except KeyError:
|
||||
# If the emoji isn't the one we care about then exit as well.
|
||||
return
|
||||
|
||||
guild = self.get_guild(payload.guild_id)
|
||||
if guild is None:
|
||||
# Check if we're still in the guild and it's cached.
|
||||
return
|
||||
|
||||
role = guild.get_role(role_id)
|
||||
if role is None:
|
||||
# Make sure the role still exists and is valid.
|
||||
return
|
||||
|
||||
# The payload for `on_raw_reaction_remove` does not provide `.member`
|
||||
# so we must get the member ourselves from the payload's `.user_id`.
|
||||
member = guild.get_member(payload.user_id)
|
||||
if member is None:
|
||||
# Makes sure the member still exists and is valid
|
||||
# Make sure the member still exists and is valid.
|
||||
return
|
||||
|
||||
try:
|
||||
# Finally, remove the role
|
||||
# Finally, remove the role.
|
||||
await member.remove_roles(role)
|
||||
except discord.HTTPException:
|
||||
# If we want to do something in case of errors we'd do it here.
|
||||
pass
|
||||
|
||||
# This bot requires the members and reactions intents.
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
client = RoleReactClient(intents=intents)
|
||||
client.run("token")
|
||||
client = MyClient(intents=intents)
|
||||
client.run('token')
|
||||
|
96
examples/secret.py
Normal file
96
examples/secret.py
Normal file
@ -0,0 +1,96 @@
|
||||
import typing
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
bot = commands.Bot(command_prefix=commands.when_mentioned, description="Nothing to see here!")
|
||||
|
||||
# the `hidden` keyword argument hides it from the help command.
|
||||
@bot.group(hidden=True)
|
||||
async def secret(ctx: commands.Context):
|
||||
"""What is this "secret" you speak of?"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send('Shh!', delete_after=5)
|
||||
|
||||
def create_overwrites(ctx, *objects):
|
||||
"""This is just a helper function that creates the overwrites for the
|
||||
voice/text channels.
|
||||
|
||||
A `discord.PermissionOverwrite` allows you to determine the permissions
|
||||
of an object, whether it be a `discord.Role` or a `discord.Member`.
|
||||
|
||||
In this case, the `view_channel` permission is being used to hide the channel
|
||||
from being viewed by whoever does not meet the criteria, thus creating a
|
||||
secret channel.
|
||||
"""
|
||||
|
||||
# a dict comprehension is being utilised here to set the same permission overwrites
|
||||
# for each `discord.Role` or `discord.Member`.
|
||||
overwrites = {
|
||||
obj: discord.PermissionOverwrite(view_channel=True)
|
||||
for obj in objects
|
||||
}
|
||||
|
||||
# prevents the default role (@everyone) from viewing the channel
|
||||
# if it isn't already allowed to view the channel.
|
||||
overwrites.setdefault(ctx.guild.default_role, discord.PermissionOverwrite(view_channel=False))
|
||||
|
||||
# makes sure the client is always allowed to view the channel.
|
||||
overwrites[ctx.guild.me] = discord.PermissionOverwrite(view_channel=True)
|
||||
|
||||
return overwrites
|
||||
|
||||
# since these commands rely on guild related features,
|
||||
# it is best to lock it to be guild-only.
|
||||
@secret.command()
|
||||
@commands.guild_only()
|
||||
async def text(ctx: commands.Context, name: str, *objects: typing.Union[discord.Role, discord.Member]):
|
||||
"""This makes a text channel with a specified name
|
||||
that is only visible to roles or members that are specified.
|
||||
"""
|
||||
|
||||
overwrites = create_overwrites(ctx, *objects)
|
||||
|
||||
await ctx.guild.create_text_channel(
|
||||
name,
|
||||
overwrites=overwrites,
|
||||
topic='Top secret text channel. Any leakage of this channel may result in serious trouble.',
|
||||
reason='Very secret business.',
|
||||
)
|
||||
|
||||
@secret.command()
|
||||
@commands.guild_only()
|
||||
async def voice(ctx: commands.Context, name: str, *objects: typing.Union[discord.Role, discord.Member]):
|
||||
"""This does the same thing as the `text` subcommand
|
||||
but instead creates a voice channel.
|
||||
"""
|
||||
|
||||
overwrites = create_overwrites(ctx, *objects)
|
||||
|
||||
await ctx.guild.create_voice_channel(
|
||||
name,
|
||||
overwrites=overwrites,
|
||||
reason='Very secret business.'
|
||||
)
|
||||
|
||||
@secret.command()
|
||||
@commands.guild_only()
|
||||
async def emoji(ctx: commands.Context, emoji: discord.PartialEmoji, *roles: discord.Role):
|
||||
"""This clones a specified emoji that only specified roles
|
||||
are allowed to use.
|
||||
"""
|
||||
|
||||
# fetch the emoji asset and read it as bytes.
|
||||
emoji_bytes = await emoji.url.read()
|
||||
|
||||
# the key parameter here is `roles`, which controls
|
||||
# what roles are able to use the emoji.
|
||||
await ctx.guild.create_custom_emoji(
|
||||
name=emoji.name,
|
||||
image=emoji_bytes,
|
||||
roles=roles,
|
||||
reason='Very secret business.'
|
||||
)
|
||||
|
||||
|
||||
bot.run('token')
|
8
setup.py
8
setup.py
@ -50,7 +50,7 @@ setup(name='enhanced-dpy',
|
||||
"Issue tracker": "https://github.com/iDutchy/discord.py/issues",
|
||||
},
|
||||
version=version,
|
||||
packages=['discord', 'discord.ext.commands', 'discord.ext.tasks'],
|
||||
packages=['discord', 'discord.types', 'discord.ext.commands', 'discord.ext.tasks'],
|
||||
license='MIT',
|
||||
description='A Python wrapper for the Discord API',
|
||||
long_description=readme,
|
||||
@ -58,17 +58,15 @@ setup(name='enhanced-dpy',
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
extras_require=extras_require,
|
||||
python_requires='>=3.5.3',
|
||||
python_requires='>=3.8.0',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Intended Audience :: Developers',
|
||||
'Natural Language :: English',
|
||||
'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.9',
|
||||
'Topic :: Internet',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
|
Loading…
x
Reference in New Issue
Block a user