Compare commits
356 Commits
Daishiky/m
...
paris-ci/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c95966ac | ||
|
|
faa3e84cfb | ||
|
|
3a71d3be5f | ||
|
|
04788d0a06 | ||
|
|
fc66c5b92d | ||
|
|
0dd4c4c08c | ||
|
|
35a9533e8d | ||
|
|
11e23c534a | ||
|
|
ee26b58c6c | ||
|
|
fa6fa6a567 | ||
|
|
2eb0ec07ab | ||
|
|
c2df574b2a | ||
|
|
7dccbace78 | ||
|
|
2247fbb23a | ||
|
|
c693945a46 | ||
|
|
746da7d54c | ||
|
|
1152f67efc | ||
|
|
5ae7940ec8 | ||
|
|
e13cbf4644 | ||
|
|
bd369c76ea | ||
|
|
9adf94e6b1 | ||
|
|
92ee2cd598 | ||
|
|
4b51e3e253 | ||
|
|
d0d2d7ea62 | ||
|
|
5a72391b72 | ||
|
|
3a421a3eb9 | ||
|
|
b2176dc0ef | ||
|
|
40127eb7b5 | ||
|
|
b9d8d3872e | ||
|
|
429c5933d9 | ||
|
|
a16f54afdb | ||
|
|
a09f89cedf | ||
|
|
c6d09a8bfa | ||
|
|
72c66a1706 | ||
|
|
4a4e73ec14 | ||
|
|
ac95b8b85b | ||
|
|
51cc7622a6 | ||
|
|
cb9a506686 | ||
|
|
7c6724fdd7 | ||
|
|
9d3962aa7a | ||
|
|
c1ce3b949f | ||
|
|
68c7c538f5 | ||
|
|
6c79714b42 | ||
|
|
4724943861 | ||
|
|
5c2945bcd4 | ||
|
|
94bbdc154c | ||
|
|
a7ae2eb1bb | ||
|
|
dd727fb6f4 | ||
|
|
ab6d592f8c | ||
|
|
2ea2693bd7 | ||
|
|
fb0c6c56e1 | ||
|
|
81e9d70b7b | ||
|
|
876b1e0f3e | ||
|
|
27556ea0a2 | ||
|
|
dbd9ed2c41 | ||
|
|
9e4bcd3df7 | ||
|
|
4b1059579e | ||
|
|
47f2d04940 | ||
|
|
be5f4ae4ab | ||
|
|
2f0a2b244e | ||
|
|
0847085661 | ||
|
|
369951fd80 | ||
|
|
bac6c2fc7b | ||
|
|
78275023cc | ||
|
|
7c40e83d10 | ||
|
|
c811932ca7 | ||
|
|
09f0ed1fba | ||
|
|
89d24cb0bc | ||
|
|
d0097c4281 | ||
|
|
4a3491cc0a | ||
|
|
8dafe4f544 | ||
|
|
2ed3e049e1 | ||
|
|
61a189c217 | ||
|
|
9f98a9a87f | ||
|
|
90a28d48d5 | ||
|
|
7b1c57ed60 | ||
|
|
2ebd5315f9 | ||
|
|
c9cdb47338 | ||
|
|
db58e628ba | ||
|
|
267fad9180 | ||
|
|
c6f3ed1af4 | ||
|
|
1b15772671 | ||
|
|
02c317d9a4 | ||
|
|
7bd1211b36 | ||
|
|
695662416a | ||
|
|
c21d12be5e | ||
|
|
d78e5d979d | ||
|
|
5a68d3a561 | ||
|
|
5a9cbc967b | ||
|
|
794327cdb4 | ||
|
|
1ae40a11b7 | ||
|
|
06743dd434 | ||
|
|
732c5384fd | ||
|
|
4d7822493f | ||
|
|
7e1f8bf1b4 | ||
|
|
52678b2eb5 | ||
|
|
b48f510e15 | ||
|
|
f321efd4de | ||
|
|
b84c199c70 | ||
|
|
c475218112 | ||
|
|
35bef7af38 | ||
|
|
f4fe247813 | ||
|
|
9ba5745e68 | ||
|
|
ef9f61a933 | ||
|
|
6874aa73c4 | ||
|
|
ff36aedf7b | ||
|
|
8bd17ede47 | ||
|
|
aeb2cfb573 | ||
|
|
263f45d05b | ||
|
|
3f60997630 | ||
|
|
97f308d219 | ||
|
|
3453992ce6 | ||
|
|
6c8f1ccbdf | ||
|
|
65db814d4a | ||
|
|
77ed476129 | ||
|
|
6cc3e572ba | ||
|
|
1bf782fcb5 | ||
|
|
1954861668 | ||
|
|
fc64ffdabd | ||
|
|
fbafe20e51 | ||
|
|
c89882441c | ||
|
|
7584834dd4 | ||
|
|
3b83f60b35 | ||
|
|
85758a75b3 | ||
|
|
d42c63e186 | ||
|
|
2ad2cab50c | ||
|
|
5e96ad9261 | ||
|
|
80fd222ca0 | ||
|
|
eda6680377 | ||
|
|
cc800796a2 | ||
|
|
ed9badcddf | ||
|
|
4c0ebc9221 | ||
|
|
cc56f31bcd | ||
|
|
98570793e4 | ||
|
|
f42e922696 | ||
|
|
f56543df15 | ||
|
|
67aabc3230 | ||
|
|
36cf3c94b4 | ||
|
|
3b55573777 | ||
|
|
ac061c31fb | ||
|
|
3c90f16bf0 | ||
|
|
3cb093c709 | ||
|
|
65439732b3 | ||
|
|
f87eaa613d | ||
|
|
5acb3a62f8 | ||
|
|
8e08bd6af2 | ||
|
|
cc8a86a4bd | ||
|
|
71fe40aafa | ||
|
|
42bab370a7 | ||
|
|
81b259ab36 | ||
|
|
c896563af4 | ||
|
|
5ad88dec72 | ||
|
|
42a538edda | ||
|
|
ef6f5d947a | ||
|
|
fb20c4c3d4 | ||
|
|
ee3e2944ba | ||
|
|
9d114fb066 | ||
|
|
9b4e820bbe | ||
|
|
5fa64e83e0 | ||
|
|
124c4a3919 | ||
|
|
ef22178dee | ||
|
|
f5727ff0d0 | ||
|
|
757cfad38f | ||
|
|
8bc489dba8 | ||
|
|
8b2241916a | ||
|
|
d5e14eb715 | ||
|
|
2a6d79078e | ||
|
|
7ebfface22 | ||
|
|
de965c2bf5 | ||
|
|
2e12f6de9c | ||
|
|
3864fb37a0 | ||
|
|
1bf7aadf94 | ||
|
|
7bad27d215 | ||
|
|
ca92f37f18 | ||
|
|
81004369dc | ||
|
|
598057ee79 | ||
|
|
c31946f29f | ||
|
|
7fde57c89a | ||
|
|
c69b20c52c | ||
|
|
6622be9f46 | ||
|
|
63974ec46d | ||
|
|
b32ad3de37 | ||
|
|
b82a0dc6fd | ||
|
|
1d4e339141 | ||
|
|
dc67d2bd85 | ||
|
|
e3037b32d5 | ||
|
|
2793fc06d5 | ||
|
|
cd7357b93a | ||
|
|
b0ec22065e | ||
|
|
83611edb66 | ||
|
|
135a7e9e5a | ||
|
|
d940486552 | ||
|
|
4c97b8efdd | ||
|
|
60c1240849 | ||
|
|
02e21a8905 | ||
|
|
3381d1e089 | ||
|
|
e762f55847 | ||
|
|
4fcbe75d3b | ||
|
|
51df7496db | ||
|
|
b705173676 | ||
|
|
66b17f5afb | ||
|
|
a8945b5784 | ||
|
|
1a8d63d54f | ||
|
|
58274eafbc | ||
|
|
7b8db49efe | ||
|
|
3b6a2b9e85 | ||
|
|
cbbd31cc9f | ||
|
|
c786a85a9b | ||
|
|
a2df6e81b8 | ||
|
|
56f4ae3a83 | ||
|
|
127b3239e9 | ||
|
|
9f3551926a | ||
|
|
69da87f455 | ||
|
|
b84717fc76 | ||
|
|
f4a861d76e | ||
|
|
686e531624 | ||
|
|
3c2674725a | ||
|
|
d60689a983 | ||
|
|
20c2664a50 | ||
|
|
1765cdffb1 | ||
|
|
368fda7272 | ||
|
|
4fbc78ba81 | ||
|
|
c250b9fc02 | ||
|
|
185b554a56 | ||
|
|
fb024546ff | ||
|
|
1c312a158a | ||
|
|
829c2d4a1a | ||
|
|
91c473db57 | ||
|
|
8e9860077d | ||
|
|
e09f64b7c9 | ||
|
|
e5607822d3 | ||
|
|
275a754abd | ||
|
|
67abfea61a | ||
|
|
f4165755a9 | ||
|
|
a55e817ffe | ||
|
|
157801bc90 | ||
|
|
8457f70477 | ||
|
|
1d7f387122 | ||
|
|
cfe93f19b1 | ||
|
|
42463bae67 | ||
|
|
0c1c9284f6 | ||
|
|
6065329c0e | ||
|
|
15bfdf66b2 | ||
|
|
ac7588f735 | ||
|
|
212d308835 | ||
|
|
7bfb0f8133 | ||
|
|
2e6c28bd60 | ||
|
|
18bf3d3a7d | ||
|
|
ddb71e2aed | ||
|
|
1c64689807 | ||
|
|
c54e43360b | ||
|
|
09f3f2111c | ||
|
|
3c8d3ab078 | ||
|
|
bee6402d84 | ||
|
|
8d74fad474 | ||
|
|
95777230b0 | ||
|
|
631a0b1e13 | ||
|
|
417353da4d | ||
|
|
b35596f7c8 | ||
|
|
cc4dced7c0 | ||
|
|
18badbc60f | ||
|
|
7fb746e6e5 | ||
|
|
aac0374baf | ||
|
|
821b6c61cb | ||
|
|
c2afa984ff | ||
|
|
fdf81089b5 | ||
|
|
e4513f70ad | ||
|
|
86f10f6dd6 | ||
|
|
304229071f | ||
|
|
fed259a83b | ||
|
|
f6fcffbab5 | ||
|
|
ef9bb79e91 | ||
|
|
6ba3d89076 | ||
|
|
9eaf1e85e4 | ||
|
|
57dbb37a52 | ||
|
|
b610998491 | ||
|
|
5dec62f4c0 | ||
|
|
a30ec197c2 | ||
|
|
74f92387ac | ||
|
|
d3ac191a67 | ||
|
|
8f9819eb4c | ||
|
|
ffea48f218 | ||
|
|
90d59bb06c | ||
|
|
0542b129c2 | ||
|
|
1f74b051a8 | ||
|
|
a6f7213c89 | ||
|
|
5ea5f32479 | ||
|
|
d21e65ce47 | ||
|
|
65d48302ad | ||
|
|
ed3c141f5e | ||
|
|
208b16ed1b | ||
|
|
9f1a96ea9b | ||
|
|
930c416ea7 | ||
|
|
da6119e04c | ||
|
|
30310b9ab6 | ||
|
|
dea92a69dc | ||
|
|
23aaa75802 | ||
|
|
496fcf8005 | ||
|
|
a8da7d03b9 | ||
|
|
ef4394f87d | ||
|
|
1209585de5 | ||
|
|
a8b3cfa592 | ||
|
|
9b94fe1ce0 | ||
|
|
7bdaa793f6 | ||
|
|
0f3f2cbeea | ||
|
|
74b07a3218 | ||
|
|
af5964358d | ||
|
|
7cbe942a64 | ||
|
|
f1fac96e33 | ||
|
|
74d8ad2013 | ||
|
|
1ecadf057e | ||
|
|
7d79b4ba55 | ||
|
|
217c2a1cc5 | ||
|
|
40cf397ce6 | ||
|
|
4c565e5299 | ||
|
|
7afcacc9a1 | ||
|
|
d85805ab6d | ||
|
|
7f91ae8b67 | ||
|
|
c54c4cb215 | ||
|
|
3151672cfe | ||
|
|
27886e5aa4 | ||
|
|
d5ad269b35 | ||
|
|
42c3ee6eed | ||
|
|
8e299e80f8 | ||
|
|
296bd069c1 | ||
|
|
b20e92efd8 | ||
|
|
353737239a | ||
|
|
ec71eb2fcb | ||
|
|
bcd3a00eaf | ||
|
|
4fee632526 | ||
|
|
4591705b55 | ||
|
|
f2d5ab6f80 | ||
|
|
ea32147d02 | ||
|
|
d58edd10a7 | ||
|
|
1efdef3ac3 | ||
|
|
3e92196a2b | ||
|
|
68aef92b37 | ||
|
|
1952060e1a | ||
|
|
eb5356cc47 | ||
|
|
d38ef88686 | ||
|
|
ca84d4e2b6 | ||
|
|
4134a17a29 | ||
|
|
9da2f349e7 | ||
|
|
d4df44375b | ||
|
|
05c123f3ab | ||
|
|
99fc950510 | ||
|
|
c3e0b6e123 | ||
|
|
e405bd5f1f | ||
|
|
a31c19563f | ||
|
|
249d94a011 | ||
|
|
d299bfef26 | ||
|
|
0903ea949f | ||
|
|
e895a53713 | ||
|
|
1c553f51fb | ||
|
|
cc6edccc0c | ||
|
|
f8bea3bb05 |
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report broken or incorrect behaviour
|
description: Report broken or incorrect behaviour
|
||||||
labels: unconfirmed bug
|
labels: unconfirmed bug
|
||||||
issue_body: true
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -73,3 +72,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have removed my token from display, if visible.
|
- label: I have removed my token from display, if visible.
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: If there is anything else to say, please do so here.
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a feature for this library
|
description: Suggest a feature for this library
|
||||||
labels: feature request
|
labels: feature request
|
||||||
issue_body: true
|
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
@@ -44,3 +43,7 @@ body:
|
|||||||
What is the current solution to the problem, if any?
|
What is the current solution to the problem, if any?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: If there is anything else to say, please do so here.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ To install the development version, do the following:
|
|||||||
Optional Packages
|
Optional Packages
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* PyNaCl (for voice support)
|
* `PyNaCl <https://pypi.org/project/PyNaCl/>`__ (for voice support)
|
||||||
|
|
||||||
Please note that on Linux installing voice you must install the following packages via your favourite package manager (e.g. ``apt``, ``dnf``, etc) before running the above commands:
|
Please note that on Linux installing voice you must install the following packages via your favourite package manager (e.g. ``apt``, ``dnf``, etc) before running the above commands:
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from .template import *
|
|||||||
from .widget import *
|
from .widget import *
|
||||||
from .object import *
|
from .object import *
|
||||||
from .reaction import *
|
from .reaction import *
|
||||||
from . import utils, opus, abc
|
from . import utils, opus, abc, ui
|
||||||
from .enums import *
|
from .enums import *
|
||||||
from .embeds import *
|
from .embeds import *
|
||||||
from .mentions import *
|
from .mentions import *
|
||||||
@@ -55,7 +55,10 @@ from .audit_logs import *
|
|||||||
from .raw_models import *
|
from .raw_models import *
|
||||||
from .team import *
|
from .team import *
|
||||||
from .sticker import *
|
from .sticker import *
|
||||||
|
from .stage_instance import *
|
||||||
from .interactions import *
|
from .interactions import *
|
||||||
|
from .components import *
|
||||||
|
from .threads import *
|
||||||
|
|
||||||
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
|
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ class Bot(commands.{base}):
|
|||||||
try:
|
try:
|
||||||
self.load_extension(cog)
|
self.load_extension(cog)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
|
print(f'Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}')
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
|
print(f'Logged on as {{self.user}} (ID: {{self.user.id}})')
|
||||||
|
|
||||||
|
|
||||||
bot = Bot()
|
bot = Bot()
|
||||||
|
|||||||
427
discord/abc.py
427
discord/abc.py
@@ -24,10 +24,9 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
import copy
|
import copy
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
from typing import Any, Dict, List, Mapping, Optional, TYPE_CHECKING, Protocol, Type, TypeVar, Union, overload, runtime_checkable
|
||||||
|
|
||||||
from .iterators import HistoryIterator
|
from .iterators import HistoryIterator
|
||||||
from .context_managers import Typing
|
from .context_managers import Typing
|
||||||
@@ -50,10 +49,25 @@ __all__ = (
|
|||||||
'Connectable',
|
'Connectable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
T = TypeVar('T', bound=VoiceProtocol)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .user import ClientUser
|
from .user import ClientUser
|
||||||
|
from .asset import Asset
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .guild import Guild
|
||||||
|
from .member import Member
|
||||||
|
from .channel import CategoryChannel
|
||||||
|
from .embeds import Embed
|
||||||
|
from .message import Message, MessageReference
|
||||||
|
from .enums import InviteTarget
|
||||||
|
from .ui.view import View
|
||||||
|
|
||||||
|
SnowflakeTime = Union["Snowflake", datetime]
|
||||||
|
|
||||||
|
MISSING = utils.MISSING
|
||||||
|
|
||||||
|
|
||||||
class _Undefined:
|
class _Undefined:
|
||||||
@@ -61,7 +75,7 @@ class _Undefined:
|
|||||||
return 'see-below'
|
return 'see-below'
|
||||||
|
|
||||||
|
|
||||||
_undefined = _Undefined()
|
_undefined: Any = _Undefined()
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -79,6 +93,7 @@ class Snowflake(Protocol):
|
|||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
The model's unique ID.
|
The model's unique ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
@@ -87,7 +102,6 @@ class Snowflake(Protocol):
|
|||||||
""":class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
|
""":class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class User(Snowflake, Protocol):
|
class User(Snowflake, Protocol):
|
||||||
"""An ABC that details the common operations on a Discord user.
|
"""An ABC that details the common operations on a Discord user.
|
||||||
@@ -106,16 +120,17 @@ class User(Snowflake, Protocol):
|
|||||||
The user's username.
|
The user's username.
|
||||||
discriminator: :class:`str`
|
discriminator: :class:`str`
|
||||||
The user's discriminator.
|
The user's discriminator.
|
||||||
avatar: Optional[:class:`str`]
|
avatar: :class:`~discord.Asset`
|
||||||
The avatar hash the user has.
|
The avatar asset the user has.
|
||||||
bot: :class:`bool`
|
bot: :class:`bool`
|
||||||
If the user is a bot account.
|
If the user is a bot account.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
discriminator: str
|
discriminator: str
|
||||||
avatar: Optional[str]
|
avatar: Asset
|
||||||
bot: bool
|
bot: bool
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -145,6 +160,7 @@ class PrivateChannel(Snowflake, Protocol):
|
|||||||
me: :class:`~discord.ClientUser`
|
me: :class:`~discord.ClientUser`
|
||||||
The user presenting yourself.
|
The user presenting yourself.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
me: ClientUser
|
me: ClientUser
|
||||||
@@ -153,11 +169,14 @@ class PrivateChannel(Snowflake, Protocol):
|
|||||||
class _Overwrites:
|
class _Overwrites:
|
||||||
__slots__ = ('id', 'allow', 'deny', 'type')
|
__slots__ = ('id', 'allow', 'deny', 'type')
|
||||||
|
|
||||||
|
ROLE = 0
|
||||||
|
MEMBER = 1
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.id = kwargs.pop('id')
|
self.id = kwargs.pop('id')
|
||||||
self.allow = int(kwargs.pop('allow_new', 0))
|
self.allow = int(kwargs.pop('allow', 0))
|
||||||
self.deny = int(kwargs.pop('deny_new', 0))
|
self.deny = int(kwargs.pop('deny', 0))
|
||||||
self.type = sys.intern(kwargs.pop('type'))
|
self.type = kwargs.pop('type')
|
||||||
|
|
||||||
def _asdict(self):
|
def _asdict(self):
|
||||||
return {
|
return {
|
||||||
@@ -167,8 +186,17 @@ class _Overwrites:
|
|||||||
'type': self.type,
|
'type': self.type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def is_role(self) -> bool:
|
||||||
|
return self.type == 0
|
||||||
|
|
||||||
class GuildChannel(Protocol):
|
def is_member(self) -> bool:
|
||||||
|
return self.type == 1
|
||||||
|
|
||||||
|
|
||||||
|
GCH = TypeVar('GCH', bound='GuildChannel')
|
||||||
|
|
||||||
|
|
||||||
|
class GuildChannel:
|
||||||
"""An ABC that details the common operations on a Discord guild channel.
|
"""An ABC that details the common operations on a Discord guild channel.
|
||||||
|
|
||||||
The following implement this ABC:
|
The following implement this ABC:
|
||||||
@@ -176,6 +204,7 @@ class GuildChannel(Protocol):
|
|||||||
- :class:`~discord.TextChannel`
|
- :class:`~discord.TextChannel`
|
||||||
- :class:`~discord.VoiceChannel`
|
- :class:`~discord.VoiceChannel`
|
||||||
- :class:`~discord.CategoryChannel`
|
- :class:`~discord.CategoryChannel`
|
||||||
|
- :class:`~discord.StageChannel`
|
||||||
|
|
||||||
This ABC must also implement :class:`~discord.abc.Snowflake`.
|
This ABC must also implement :class:`~discord.abc.Snowflake`.
|
||||||
|
|
||||||
@@ -194,16 +223,38 @@ class GuildChannel(Protocol):
|
|||||||
The position in the channel list. This is a number that starts at 0.
|
The position in the channel list. This is a number that starts at 0.
|
||||||
e.g. the top channel is position 0.
|
e.g. the top channel is position 0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __str__(self):
|
id: int
|
||||||
|
name: str
|
||||||
|
guild: Guild
|
||||||
|
type: ChannelType
|
||||||
|
_state: ConnectionState
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
def __init__(self, *, state: ConnectionState, guild: Guild, data: Dict[str, Any]):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _sorting_bucket(self):
|
def _sorting_bucket(self) -> int:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def _move(self, position, parent_id=None, lock_permissions=False, *, reason):
|
def _update(self, guild: Guild, data: Dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def _move(
|
||||||
|
self,
|
||||||
|
position: int,
|
||||||
|
parent_id: Optional[Any] = None,
|
||||||
|
lock_permissions: bool = False,
|
||||||
|
*,
|
||||||
|
reason: Optional[str],
|
||||||
|
):
|
||||||
if position < 0:
|
if position < 0:
|
||||||
raise InvalidArgument('Channel position cannot be less than 0.')
|
raise InvalidArgument('Channel position cannot be less than 0.')
|
||||||
|
|
||||||
@@ -256,6 +307,13 @@ class GuildChannel(Protocol):
|
|||||||
else:
|
else:
|
||||||
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
|
||||||
|
|
||||||
|
try:
|
||||||
|
video_quality_mode = options.pop('video_quality_mode')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
options['video_quality_mode'] = int(video_quality_mode)
|
||||||
|
|
||||||
lock_permissions = options.pop('sync_permissions', False)
|
lock_permissions = options.pop('sync_permissions', False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -279,19 +337,19 @@ class GuildChannel(Protocol):
|
|||||||
perms = []
|
perms = []
|
||||||
for target, perm in overwrites.items():
|
for target, perm in overwrites.items():
|
||||||
if not isinstance(perm, PermissionOverwrite):
|
if not isinstance(perm, PermissionOverwrite):
|
||||||
raise InvalidArgument('Expected PermissionOverwrite received {0.__name__}'.format(type(perm)))
|
raise InvalidArgument(f'Expected PermissionOverwrite received {perm.__class__.__name__}')
|
||||||
|
|
||||||
allow, deny = perm.pair()
|
allow, deny = perm.pair()
|
||||||
payload = {
|
payload = {
|
||||||
'allow': allow.value,
|
'allow': allow.value,
|
||||||
'deny': deny.value,
|
'deny': deny.value,
|
||||||
'id': target.id
|
'id': target.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(target, Role):
|
if isinstance(target, Role):
|
||||||
payload['type'] = 'role'
|
payload['type'] = _Overwrites.ROLE
|
||||||
else:
|
else:
|
||||||
payload['type'] = 'member'
|
payload['type'] = _Overwrites.MEMBER
|
||||||
|
|
||||||
perms.append(payload)
|
perms.append(payload)
|
||||||
options['permission_overwrites'] = perms
|
options['permission_overwrites'] = perms
|
||||||
@@ -318,7 +376,7 @@ class GuildChannel(Protocol):
|
|||||||
overridden_id = int(overridden.pop('id'))
|
overridden_id = int(overridden.pop('id'))
|
||||||
self._overwrites.append(_Overwrites(id=overridden_id, **overridden))
|
self._overwrites.append(_Overwrites(id=overridden_id, **overridden))
|
||||||
|
|
||||||
if overridden['type'] == 'member':
|
if overridden['type'] == _Overwrites.MEMBER:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if overridden_id == everyone_id:
|
if overridden_id == everyone_id:
|
||||||
@@ -335,12 +393,12 @@ class GuildChannel(Protocol):
|
|||||||
tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index]
|
tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_roles(self):
|
def changed_roles(self) -> List[Role]:
|
||||||
"""List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from
|
"""List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from
|
||||||
their default values in the :attr:`~discord.Guild.roles` attribute."""
|
their default values in the :attr:`~discord.Guild.roles` attribute."""
|
||||||
ret = []
|
ret = []
|
||||||
g = self.guild
|
g = self.guild
|
||||||
for overwrite in filter(lambda o: o.type == 'role', self._overwrites):
|
for overwrite in filter(lambda o: o.is_role(), self._overwrites):
|
||||||
role = g.get_role(overwrite.id)
|
role = g.get_role(overwrite.id)
|
||||||
if role is None:
|
if role is None:
|
||||||
continue
|
continue
|
||||||
@@ -351,16 +409,16 @@ class GuildChannel(Protocol):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mention(self):
|
def mention(self) -> str:
|
||||||
""":class:`str`: The string that allows you to mention the channel."""
|
""":class:`str`: The string that allows you to mention the channel."""
|
||||||
return f'<#{self.id}>'
|
return f'<#{self.id}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime:
|
||||||
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
||||||
return utils.snowflake_time(self.id)
|
return utils.snowflake_time(self.id)
|
||||||
|
|
||||||
def overwrites_for(self, obj):
|
def overwrites_for(self, obj: Union[Role, User]) -> PermissionOverwrite:
|
||||||
"""Returns the channel-specific overwrites for a member or a role.
|
"""Returns the channel-specific overwrites for a member or a role.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -376,9 +434,9 @@ class GuildChannel(Protocol):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(obj, User):
|
if isinstance(obj, User):
|
||||||
predicate = lambda p: p.type == 'member'
|
predicate = lambda p: p.is_member()
|
||||||
elif isinstance(obj, Role):
|
elif isinstance(obj, Role):
|
||||||
predicate = lambda p: p.type == 'role'
|
predicate = lambda p: p.is_role()
|
||||||
else:
|
else:
|
||||||
predicate = lambda p: True
|
predicate = lambda p: True
|
||||||
|
|
||||||
@@ -391,7 +449,7 @@ class GuildChannel(Protocol):
|
|||||||
return PermissionOverwrite()
|
return PermissionOverwrite()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def overwrites(self):
|
def overwrites(self) -> Mapping[Union[Role, Member], PermissionOverwrite]:
|
||||||
"""Returns all of the channel's overwrites.
|
"""Returns all of the channel's overwrites.
|
||||||
|
|
||||||
This is returned as a dictionary where the key contains the target which
|
This is returned as a dictionary where the key contains the target which
|
||||||
@@ -408,10 +466,11 @@ class GuildChannel(Protocol):
|
|||||||
allow = Permissions(ow.allow)
|
allow = Permissions(ow.allow)
|
||||||
deny = Permissions(ow.deny)
|
deny = Permissions(ow.deny)
|
||||||
overwrite = PermissionOverwrite.from_pair(allow, deny)
|
overwrite = PermissionOverwrite.from_pair(allow, deny)
|
||||||
|
target = None
|
||||||
|
|
||||||
if ow.type == 'role':
|
if ow.is_role():
|
||||||
target = self.guild.get_role(ow.id)
|
target = self.guild.get_role(ow.id)
|
||||||
elif ow.type == 'member':
|
elif ow.is_member():
|
||||||
target = self.guild.get_member(ow.id)
|
target = self.guild.get_member(ow.id)
|
||||||
|
|
||||||
# TODO: There is potential data loss here in the non-chunked
|
# TODO: There is potential data loss here in the non-chunked
|
||||||
@@ -424,7 +483,7 @@ class GuildChannel(Protocol):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def category(self):
|
def category(self) -> Optional[CategoryChannel]:
|
||||||
"""Optional[:class:`~discord.CategoryChannel`]: The category this channel belongs to.
|
"""Optional[:class:`~discord.CategoryChannel`]: The category this channel belongs to.
|
||||||
|
|
||||||
If there is no category then this is ``None``.
|
If there is no category then this is ``None``.
|
||||||
@@ -432,7 +491,7 @@ class GuildChannel(Protocol):
|
|||||||
return self.guild.get_channel(self.category_id)
|
return self.guild.get_channel(self.category_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def permissions_synced(self):
|
def permissions_synced(self) -> bool:
|
||||||
""":class:`bool`: Whether or not the permissions for this channel are synced with the
|
""":class:`bool`: Whether or not the permissions for this channel are synced with the
|
||||||
category it belongs to.
|
category it belongs to.
|
||||||
|
|
||||||
@@ -443,8 +502,9 @@ class GuildChannel(Protocol):
|
|||||||
category = self.guild.get_channel(self.category_id)
|
category = self.guild.get_channel(self.category_id)
|
||||||
return bool(category and category.overwrites == self.overwrites)
|
return bool(category and category.overwrites == self.overwrites)
|
||||||
|
|
||||||
def permissions_for(self, member):
|
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
|
||||||
"""Handles permission resolution for the current :class:`~discord.Member`.
|
"""Handles permission resolution for the :class:`~discord.Member`
|
||||||
|
or :class:`~discord.Role`.
|
||||||
|
|
||||||
This function takes into consideration the following cases:
|
This function takes into consideration the following cases:
|
||||||
|
|
||||||
@@ -453,15 +513,27 @@ class GuildChannel(Protocol):
|
|||||||
- Channel overrides
|
- Channel overrides
|
||||||
- Member overrides
|
- Member overrides
|
||||||
|
|
||||||
|
If a :class:`~discord.Role` is passed, then it checks the permissions
|
||||||
|
someone with that role would have, which is essentially:
|
||||||
|
|
||||||
|
- The default role permissions
|
||||||
|
- The default role permission overwrites
|
||||||
|
- The permission overwrites of the role used as a parameter
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
The object passed in can now be a role object.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
member: :class:`~discord.Member`
|
obj: Union[:class:`~discord.Member`, :class:`~discord.Role`]
|
||||||
The member to resolve permissions for.
|
The object to resolve permissions for. This could be either
|
||||||
|
a member or a role. If it's a role then member overwrites
|
||||||
|
are not computed.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
:class:`~discord.Permissions`
|
:class:`~discord.Permissions`
|
||||||
The resolved permissions for the member.
|
The resolved permissions for the member or role.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The current cases can be explained as:
|
# The current cases can be explained as:
|
||||||
@@ -478,12 +550,35 @@ class GuildChannel(Protocol):
|
|||||||
# The operation first takes into consideration the denied
|
# The operation first takes into consideration the denied
|
||||||
# and then the allowed.
|
# and then the allowed.
|
||||||
|
|
||||||
if self.guild.owner_id == member.id:
|
if self.guild.owner_id == obj.id:
|
||||||
return Permissions.all()
|
return Permissions.all()
|
||||||
|
|
||||||
default = self.guild.default_role
|
default = self.guild.default_role
|
||||||
base = Permissions(default.permissions.value)
|
base = Permissions(default.permissions.value)
|
||||||
roles = member._roles
|
|
||||||
|
# Handle the role case first
|
||||||
|
if isinstance(obj, Role):
|
||||||
|
if obj.is_default():
|
||||||
|
overwrite = utils.get(self._overwrites, type=_Overwrites.ROLE, id=obj.id)
|
||||||
|
if overwrite is not None:
|
||||||
|
base.handle_overwrite(overwrite.allow, overwrite.deny)
|
||||||
|
return base
|
||||||
|
|
||||||
|
denies = 0
|
||||||
|
allows = 0
|
||||||
|
guild_id = self.guild.id
|
||||||
|
for overwrite in self._overwrites:
|
||||||
|
if not overwrite.is_role():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if overwrite.id in (obj.id, guild_id):
|
||||||
|
denies |= overwrite.deny
|
||||||
|
allows |= overwrite.allow
|
||||||
|
|
||||||
|
base.handle_overwrite(allows, denies)
|
||||||
|
return base
|
||||||
|
|
||||||
|
roles = obj._roles
|
||||||
get_role = self.guild.get_role
|
get_role = self.guild.get_role
|
||||||
|
|
||||||
# Apply guild roles that the member has.
|
# Apply guild roles that the member has.
|
||||||
@@ -513,7 +608,7 @@ class GuildChannel(Protocol):
|
|||||||
|
|
||||||
# Apply channel specific role permission overwrites
|
# Apply channel specific role permission overwrites
|
||||||
for overwrite in remaining_overwrites:
|
for overwrite in remaining_overwrites:
|
||||||
if overwrite.type == 'role' and roles.has(overwrite.id):
|
if overwrite.is_role() and roles.has(overwrite.id):
|
||||||
denies |= overwrite.deny
|
denies |= overwrite.deny
|
||||||
allows |= overwrite.allow
|
allows |= overwrite.allow
|
||||||
|
|
||||||
@@ -521,7 +616,7 @@ class GuildChannel(Protocol):
|
|||||||
|
|
||||||
# Apply member specific permission overwrites
|
# Apply member specific permission overwrites
|
||||||
for overwrite in remaining_overwrites:
|
for overwrite in remaining_overwrites:
|
||||||
if overwrite.type == 'member' and overwrite.id == member.id:
|
if overwrite.is_member() and overwrite.id == obj.id:
|
||||||
base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny)
|
base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -540,12 +635,12 @@ class GuildChannel(Protocol):
|
|||||||
|
|
||||||
return base
|
return base
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Deletes the channel.
|
Deletes the channel.
|
||||||
|
|
||||||
You must have :attr:`~Permissions.manage_channels` permission to use this.
|
You must have :attr:`~discord.Permissions.manage_channels` permission to use this.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
@@ -564,7 +659,34 @@ class GuildChannel(Protocol):
|
|||||||
"""
|
"""
|
||||||
await self._state.http.delete_channel(self.id, reason=reason)
|
await self._state.http.delete_channel(self.id, reason=reason)
|
||||||
|
|
||||||
async def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions):
|
@overload
|
||||||
|
async def set_permissions(
|
||||||
|
self,
|
||||||
|
target: Union[Member, Role],
|
||||||
|
*,
|
||||||
|
overwrite: Optional[Union[PermissionOverwrite, _Undefined]] = ...,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def set_permissions(
|
||||||
|
self,
|
||||||
|
target: Union[Member, Role],
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
**permissions: bool,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def set_permissions(
|
||||||
|
self,
|
||||||
|
target,
|
||||||
|
*,
|
||||||
|
overwrite=_undefined,
|
||||||
|
reason=None,
|
||||||
|
**permissions
|
||||||
|
):
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
Sets the channel specific permission overwrites for a target in the
|
Sets the channel specific permission overwrites for a target in the
|
||||||
@@ -582,7 +704,11 @@ class GuildChannel(Protocol):
|
|||||||
If the ``overwrite`` parameter is ``None``, then the permission
|
If the ``overwrite`` parameter is ``None``, then the permission
|
||||||
overwrites are deleted.
|
overwrites are deleted.
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_roles` permission to use this.
|
You must have the :attr:`~discord.Permissions.manage_roles` permission to use this.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This method *replaces* the old overwrites with the ones given.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
----------
|
----------
|
||||||
@@ -632,13 +758,13 @@ class GuildChannel(Protocol):
|
|||||||
http = self._state.http
|
http = self._state.http
|
||||||
|
|
||||||
if isinstance(target, User):
|
if isinstance(target, User):
|
||||||
perm_type = 'member'
|
perm_type = _Overwrites.MEMBER
|
||||||
elif isinstance(target, Role):
|
elif isinstance(target, Role):
|
||||||
perm_type = 'role'
|
perm_type = _Overwrites.ROLE
|
||||||
else:
|
else:
|
||||||
raise InvalidArgument('target parameter must be either Member or Role')
|
raise InvalidArgument('target parameter must be either Member or Role')
|
||||||
|
|
||||||
if isinstance(overwrite, _Undefined):
|
if overwrite is _undefined:
|
||||||
if len(permissions) == 0:
|
if len(permissions) == 0:
|
||||||
raise InvalidArgument('No overwrite provided.')
|
raise InvalidArgument('No overwrite provided.')
|
||||||
try:
|
try:
|
||||||
@@ -659,10 +785,14 @@ class GuildChannel(Protocol):
|
|||||||
else:
|
else:
|
||||||
raise InvalidArgument('Invalid overwrite type provided.')
|
raise InvalidArgument('Invalid overwrite type provided.')
|
||||||
|
|
||||||
async def _clone_impl(self, base_attrs, *, name=None, reason=None):
|
async def _clone_impl(
|
||||||
base_attrs['permission_overwrites'] = [
|
self: GCH,
|
||||||
x._asdict() for x in self._overwrites
|
base_attrs: Dict[str, Any],
|
||||||
]
|
*,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> GCH:
|
||||||
|
base_attrs['permission_overwrites'] = [x._asdict() for x in self._overwrites]
|
||||||
base_attrs['parent_id'] = self.category_id
|
base_attrs['parent_id'] = self.category_id
|
||||||
base_attrs['name'] = name or self.name
|
base_attrs['name'] = name or self.name
|
||||||
guild_id = self.guild.id
|
guild_id = self.guild.id
|
||||||
@@ -674,7 +804,7 @@ class GuildChannel(Protocol):
|
|||||||
self.guild._channels[obj.id] = obj
|
self.guild._channels[obj.id] = obj
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self: GCH, *, name: Optional[str] = None, reason: Optional[str] = None) -> GCH:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Clones this channel. This creates a channel with the same properties
|
Clones this channel. This creates a channel with the same properties
|
||||||
@@ -707,12 +837,60 @@ class GuildChannel(Protocol):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def move(self, **kwargs):
|
@overload
|
||||||
|
async def move(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
beginning: bool,
|
||||||
|
offset: int = MISSING,
|
||||||
|
category: Optional[Snowflake] = MISSING,
|
||||||
|
sync_permissions: bool = MISSING,
|
||||||
|
reason: Optional[str] = MISSING,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def move(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
end: bool,
|
||||||
|
offset: int = MISSING,
|
||||||
|
category: Optional[Snowflake] = MISSING,
|
||||||
|
sync_permissions: bool = MISSING,
|
||||||
|
reason: str = MISSING,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def move(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
before: Snowflake,
|
||||||
|
offset: int = MISSING,
|
||||||
|
category: Optional[Snowflake] = MISSING,
|
||||||
|
sync_permissions: bool = MISSING,
|
||||||
|
reason: str = MISSING,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def move(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
after: Snowflake,
|
||||||
|
offset: int = MISSING,
|
||||||
|
category: Optional[Snowflake] = MISSING,
|
||||||
|
sync_permissions: bool = MISSING,
|
||||||
|
reason: str = MISSING,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def move(self, **kwargs) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
A rich interface to help move a channel relative to other channels.
|
A rich interface to help move a channel relative to other channels.
|
||||||
|
|
||||||
If exact position movement is required, :meth:`edit` should be used instead.
|
If exact position movement is required, ``edit`` should be used instead.
|
||||||
|
|
||||||
You must have the :attr:`~discord.Permissions.manage_channels` permission to
|
You must have the :attr:`~discord.Permissions.manage_channels` permission to
|
||||||
do this.
|
do this.
|
||||||
@@ -734,10 +912,10 @@ class GuildChannel(Protocol):
|
|||||||
Whether to move the channel to the end of the
|
Whether to move the channel to the end of the
|
||||||
channel list (or category if given).
|
channel list (or category if given).
|
||||||
This is mutually exclusive with ``beginning``, ``before``, and ``after``.
|
This is mutually exclusive with ``beginning``, ``before``, and ``after``.
|
||||||
before: :class:`abc.Snowflake`
|
before: :class:`~discord.abc.Snowflake`
|
||||||
The channel that should be before our current channel.
|
The channel that should be before our current channel.
|
||||||
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
|
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
|
||||||
after: :class:`abc.Snowflake`
|
after: :class:`~discord.abc.Snowflake`
|
||||||
The channel that should be after our current channel.
|
The channel that should be after our current channel.
|
||||||
This is mutually exclusive with ``beginning``, ``end``, and ``before``.
|
This is mutually exclusive with ``beginning``, ``end``, and ``before``.
|
||||||
offset: :class:`int`
|
offset: :class:`int`
|
||||||
@@ -747,7 +925,7 @@ class GuildChannel(Protocol):
|
|||||||
while a negative number moves it above. Note that this
|
while a negative number moves it above. Note that this
|
||||||
number is relative and computed after the ``beginning``,
|
number is relative and computed after the ``beginning``,
|
||||||
``end``, ``before``, and ``after`` parameters.
|
``end``, ``before``, and ``after`` parameters.
|
||||||
category: Optional[:class:`abc.Snowflake`]
|
category: Optional[:class:`~discord.abc.Snowflake`]
|
||||||
The category to move this channel under.
|
The category to move this channel under.
|
||||||
If ``None`` is given then it moves it out of the category.
|
If ``None`` is given then it moves it out of the category.
|
||||||
This parameter is ignored if moving a category channel.
|
This parameter is ignored if moving a category channel.
|
||||||
@@ -776,8 +954,9 @@ class GuildChannel(Protocol):
|
|||||||
raise InvalidArgument('Only one of [before, after, end, beginning] can be used.')
|
raise InvalidArgument('Only one of [before, after, end, beginning] can be used.')
|
||||||
|
|
||||||
bucket = self._sorting_bucket
|
bucket = self._sorting_bucket
|
||||||
parent_id = kwargs.get('category', ...)
|
parent_id = kwargs.get('category', MISSING)
|
||||||
if parent_id not in (..., None):
|
# fmt: off
|
||||||
|
if parent_id not in (MISSING, None):
|
||||||
parent_id = parent_id.id
|
parent_id = parent_id.id
|
||||||
channels = [
|
channels = [
|
||||||
ch
|
ch
|
||||||
@@ -792,6 +971,7 @@ class GuildChannel(Protocol):
|
|||||||
if ch._sorting_bucket == bucket
|
if ch._sorting_bucket == bucket
|
||||||
and ch.category_id == self.category_id
|
and ch.category_id == self.category_id
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
channels.sort(key=lambda c: (c.position, c.id))
|
channels.sort(key=lambda c: (c.position, c.id))
|
||||||
|
|
||||||
@@ -821,18 +1001,29 @@ class GuildChannel(Protocol):
|
|||||||
reason = kwargs.get('reason')
|
reason = kwargs.get('reason')
|
||||||
for index, channel in enumerate(channels):
|
for index, channel in enumerate(channels):
|
||||||
d = {'id': channel.id, 'position': index}
|
d = {'id': channel.id, 'position': index}
|
||||||
if parent_id is not ... and channel.id == self.id:
|
if parent_id is not MISSING and channel.id == self.id:
|
||||||
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
|
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
|
||||||
payload.append(d)
|
payload.append(d)
|
||||||
|
|
||||||
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
|
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
|
||||||
|
|
||||||
async def create_invite(self, *, reason=None, **fields):
|
async def create_invite(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
max_age: int = 0,
|
||||||
|
max_uses: int = 0,
|
||||||
|
temporary: bool = False,
|
||||||
|
unique: bool = True,
|
||||||
|
target_type: Optional[InviteTarget] = None,
|
||||||
|
target_user: Optional[User] = None,
|
||||||
|
target_application_id: Optional[int] = None
|
||||||
|
) -> Invite:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Creates an instant invite from a text or voice channel.
|
Creates an instant invite from a text or voice channel.
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.create_instant_invite` permission to
|
You must have the :attr:`~discord.Permissions.create_instant_invite` permission to
|
||||||
do this.
|
do this.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -852,6 +1043,20 @@ class GuildChannel(Protocol):
|
|||||||
invite.
|
invite.
|
||||||
reason: Optional[:class:`str`]
|
reason: Optional[:class:`str`]
|
||||||
The reason for creating this invite. Shows up on the audit log.
|
The reason for creating this invite. Shows up on the audit log.
|
||||||
|
target_type: Optional[:class:`InviteTarget`]
|
||||||
|
The type of target for the voice channel invite, if any.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
target_user: Optional[:class:`User`]
|
||||||
|
The user whose stream to display for this invite, required if `target_type` is `TargetType.stream`. The user must be streaming in the channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
target_application_id:: Optional[:class:`int`]
|
||||||
|
The id of the embedded application for the invite, required if `target_type` is `TargetType.embedded_application`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
@@ -867,15 +1072,25 @@ class GuildChannel(Protocol):
|
|||||||
The invite that was created.
|
The invite that was created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = await self._state.http.create_invite(self.id, reason=reason, **fields)
|
data = await self._state.http.create_invite(
|
||||||
|
self.id,
|
||||||
|
reason=reason,
|
||||||
|
max_age=max_age,
|
||||||
|
max_uses=max_uses,
|
||||||
|
temporary=temporary,
|
||||||
|
unique=unique,
|
||||||
|
target_type=target_type.value if target_type else None,
|
||||||
|
target_user_id=target_user.id if target_user else None,
|
||||||
|
target_application_id=target_application_id
|
||||||
|
)
|
||||||
return Invite.from_incomplete(data=data, state=self._state)
|
return Invite.from_incomplete(data=data, state=self._state)
|
||||||
|
|
||||||
async def invites(self):
|
async def invites(self) -> List[Invite]:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Returns a list of all active instant invites from this channel.
|
Returns a list of all active instant invites from this channel.
|
||||||
|
|
||||||
You must have :attr:`~Permissions.manage_channels` to get this information.
|
You must have :attr:`~discord.Permissions.manage_channels` to get this information.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
@@ -892,14 +1107,8 @@ class GuildChannel(Protocol):
|
|||||||
|
|
||||||
state = self._state
|
state = self._state
|
||||||
data = await state.http.invites_from_channel(self.id)
|
data = await state.http.invites_from_channel(self.id)
|
||||||
result = []
|
guild = self.guild
|
||||||
|
return [Invite(state=state, data=invite, channel=self, guild=guild) for invite in data]
|
||||||
for invite in data:
|
|
||||||
invite['channel'] = self
|
|
||||||
invite['guild'] = self.guild
|
|
||||||
result.append(Invite(state=state, data=invite))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Messageable(Protocol):
|
class Messageable(Protocol):
|
||||||
@@ -926,10 +1135,44 @@ class Messageable(Protocol):
|
|||||||
async def _get_channel(self):
|
async def _get_channel(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
content: Optional[str] =...,
|
||||||
|
*,
|
||||||
|
tts: bool = ...,
|
||||||
|
embed: Embed = ...,
|
||||||
|
file: File = ...,
|
||||||
|
delete_after: int = ...,
|
||||||
|
nonce: Union[str, int] = ...,
|
||||||
|
allowed_mentions: AllowedMentions = ...,
|
||||||
|
reference: Union[Message, MessageReference] = ...,
|
||||||
|
mention_author: bool = ...,
|
||||||
|
view: View = ...,
|
||||||
|
) -> Message:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
content: Optional[str] = ...,
|
||||||
|
*,
|
||||||
|
tts: bool = ...,
|
||||||
|
embed: Embed = ...,
|
||||||
|
files: List[File] = ...,
|
||||||
|
delete_after: int = ...,
|
||||||
|
nonce: Union[str, int] = ...,
|
||||||
|
allowed_mentions: AllowedMentions = ...,
|
||||||
|
reference: Union[Message, MessageReference] = ...,
|
||||||
|
mention_author: bool = ...,
|
||||||
|
view: View = ...,
|
||||||
|
) -> Message:
|
||||||
|
...
|
||||||
|
|
||||||
async def send(self, content=None, *, tts=False, embed=None, file=None,
|
async def send(self, content=None, *, tts=False, embed=None, file=None,
|
||||||
files=None, delete_after=None, nonce=None,
|
files=None, delete_after=None, nonce=None,
|
||||||
allowed_mentions=None, reference=None,
|
allowed_mentions=None, reference=None,
|
||||||
mention_author=None):
|
mention_author=None, view=None):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Sends a message to the destination with the content given.
|
Sends a message to the destination with the content given.
|
||||||
@@ -987,6 +1230,10 @@ class Messageable(Protocol):
|
|||||||
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
|
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
|
view: :class:`discord.ui.View`
|
||||||
|
A Discord UI View to add to the message.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
--------
|
--------
|
||||||
@@ -1030,6 +1277,14 @@ class Messageable(Protocol):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise InvalidArgument('reference parameter must be Message or MessageReference') from None
|
raise InvalidArgument('reference parameter must be Message or MessageReference') from None
|
||||||
|
|
||||||
|
if view:
|
||||||
|
if not hasattr(view, '__discord_ui_view__'):
|
||||||
|
raise InvalidArgument(f'view parameter must be View not {view.__class__!r}')
|
||||||
|
|
||||||
|
components = view.to_components()
|
||||||
|
else:
|
||||||
|
components = None
|
||||||
|
|
||||||
if file is not None and files is not None:
|
if file is not None and files is not None:
|
||||||
raise InvalidArgument('cannot pass both file and files parameter to send()')
|
raise InvalidArgument('cannot pass both file and files parameter to send()')
|
||||||
|
|
||||||
@@ -1040,7 +1295,7 @@ class Messageable(Protocol):
|
|||||||
try:
|
try:
|
||||||
data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions,
|
data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions,
|
||||||
content=content, tts=tts, embed=embed, nonce=nonce,
|
content=content, tts=tts, embed=embed, nonce=nonce,
|
||||||
message_reference=reference)
|
message_reference=reference, components=components)
|
||||||
finally:
|
finally:
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
@@ -1053,16 +1308,19 @@ class Messageable(Protocol):
|
|||||||
try:
|
try:
|
||||||
data = await state.http.send_files(channel.id, files=files, content=content, tts=tts,
|
data = await state.http.send_files(channel.id, files=files, content=content, tts=tts,
|
||||||
embed=embed, nonce=nonce, allowed_mentions=allowed_mentions,
|
embed=embed, nonce=nonce, allowed_mentions=allowed_mentions,
|
||||||
message_reference=reference)
|
message_reference=reference, components=components)
|
||||||
finally:
|
finally:
|
||||||
for f in files:
|
for f in files:
|
||||||
f.close()
|
f.close()
|
||||||
else:
|
else:
|
||||||
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
|
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
|
||||||
nonce=nonce, allowed_mentions=allowed_mentions,
|
nonce=nonce, allowed_mentions=allowed_mentions,
|
||||||
message_reference=reference)
|
message_reference=reference, components=components)
|
||||||
|
|
||||||
ret = state.create_message(channel=channel, data=data)
|
ret = state.create_message(channel=channel, data=data)
|
||||||
|
if view:
|
||||||
|
state.store_view(view, ret.id)
|
||||||
|
|
||||||
if delete_after is not None:
|
if delete_after is not None:
|
||||||
await ret.delete(delay=delete_after)
|
await ret.delete(delay=delete_after)
|
||||||
return ret
|
return ret
|
||||||
@@ -1089,10 +1347,11 @@ class Messageable(Protocol):
|
|||||||
This means that both ``with`` and ``async with`` work with this.
|
This means that both ``with`` and ``async with`` work with this.
|
||||||
|
|
||||||
Example Usage: ::
|
Example Usage: ::
|
||||||
|
|
||||||
async with channel.typing():
|
async with channel.typing():
|
||||||
# do expensive stuff here
|
# simulate something heavy
|
||||||
await channel.send('done!')
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
await channel.send('done!')
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return Typing(self)
|
return Typing(self)
|
||||||
@@ -1102,8 +1361,6 @@ class Messageable(Protocol):
|
|||||||
|
|
||||||
Retrieves a single :class:`~discord.Message` from the destination.
|
Retrieves a single :class:`~discord.Message` from the destination.
|
||||||
|
|
||||||
This can only be used by bot accounts.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
------------
|
------------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
@@ -1158,7 +1415,7 @@ class Messageable(Protocol):
|
|||||||
def history(self, *, limit=100, before=None, after=None, around=None, oldest_first=None):
|
def history(self, *, limit=100, before=None, after=None, around=None, oldest_first=None):
|
||||||
"""Returns an :class:`~discord.AsyncIterator` that enables receiving the destination's message history.
|
"""Returns an :class:`~discord.AsyncIterator` that enables receiving the destination's message history.
|
||||||
|
|
||||||
You must have :attr:`~Permissions.read_message_history` permissions to use this.
|
You must have :attr:`~discord.Permissions.read_message_history` permissions to use this.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
---------
|
---------
|
||||||
@@ -1223,12 +1480,14 @@ class Connectable(Protocol):
|
|||||||
The following implement this ABC:
|
The following implement this ABC:
|
||||||
|
|
||||||
- :class:`~discord.VoiceChannel`
|
- :class:`~discord.VoiceChannel`
|
||||||
|
- :class:`~discord.StageChannel`
|
||||||
|
|
||||||
Note
|
Note
|
||||||
----
|
----
|
||||||
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
|
||||||
checks.
|
checks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def _get_voice_client_key(self):
|
def _get_voice_client_key(self):
|
||||||
@@ -1237,7 +1496,7 @@ class Connectable(Protocol):
|
|||||||
def _get_voice_state_pair(self):
|
def _get_voice_state_pair(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def connect(self, *, timeout=60.0, reconnect=True, cls=VoiceClient):
|
async def connect(self, *, timeout: float = 60.0, reconnect: bool = True, cls: Type[T] = VoiceClient) -> T:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Connects to voice and creates a :class:`VoiceClient` to establish
|
Connects to voice and creates a :class:`VoiceClient` to establish
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .enums import ActivityType, try_enum
|
from .enums import ActivityType, try_enum
|
||||||
@@ -71,6 +74,9 @@ type: int
|
|||||||
sync_id: str
|
sync_id: str
|
||||||
session_id: str
|
session_id: str
|
||||||
flags: int
|
flags: int
|
||||||
|
buttons: list[dict]
|
||||||
|
label: str (max: 32)
|
||||||
|
url: str (max: 512)
|
||||||
|
|
||||||
There are also activity flags which are mostly uninteresting for the library atm.
|
There are also activity flags which are mostly uninteresting for the library atm.
|
||||||
|
|
||||||
@@ -84,6 +90,15 @@ t.ActivityFlags = {
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.activity import (
|
||||||
|
ActivityTimestamps,
|
||||||
|
ActivityParty,
|
||||||
|
ActivityAssets,
|
||||||
|
ActivityButton,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseActivity:
|
class BaseActivity:
|
||||||
"""The base activity that all user-settable activities inherit from.
|
"""The base activity that all user-settable activities inherit from.
|
||||||
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
|
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
|
||||||
@@ -102,6 +117,7 @@ class BaseActivity:
|
|||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_created_at',)
|
__slots__ = ('_created_at',)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -116,6 +132,7 @@ class BaseActivity:
|
|||||||
if self._created_at is not None:
|
if self._created_at is not None:
|
||||||
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
|
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
|
||||||
|
|
||||||
|
|
||||||
class Activity(BaseActivity):
|
class Activity(BaseActivity):
|
||||||
"""Represents an activity in Discord.
|
"""Represents an activity in Discord.
|
||||||
|
|
||||||
@@ -164,27 +181,51 @@ class Activity(BaseActivity):
|
|||||||
|
|
||||||
- ``id``: A string representing the party ID.
|
- ``id``: A string representing the party ID.
|
||||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
||||||
|
buttons: List[:class:`dict`]
|
||||||
|
An list of dictionaries representing custom buttons shown in a rich presence.
|
||||||
|
Each dictionary contains the following keys:
|
||||||
|
|
||||||
|
- ``label``: A string representing the text shown on the button.
|
||||||
|
- ``url``: A string representing the URL opened upon clicking the button.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
emoji: Optional[:class:`PartialEmoji`]
|
emoji: Optional[:class:`PartialEmoji`]
|
||||||
The emoji that belongs to this activity.
|
The emoji that belongs to this activity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('state', 'details', '_created_at', 'timestamps', 'assets', 'party',
|
__slots__ = (
|
||||||
'flags', 'sync_id', 'session_id', 'type', 'name', 'url',
|
'state',
|
||||||
'application_id', 'emoji')
|
'details',
|
||||||
|
'_created_at',
|
||||||
|
'timestamps',
|
||||||
|
'assets',
|
||||||
|
'party',
|
||||||
|
'flags',
|
||||||
|
'sync_id',
|
||||||
|
'session_id',
|
||||||
|
'type',
|
||||||
|
'name',
|
||||||
|
'url',
|
||||||
|
'application_id',
|
||||||
|
'emoji',
|
||||||
|
'buttons',
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.state = kwargs.pop('state', None)
|
self.state = kwargs.pop('state', None)
|
||||||
self.details = kwargs.pop('details', None)
|
self.details = kwargs.pop('details', None)
|
||||||
self.timestamps = kwargs.pop('timestamps', {})
|
self.timestamps: ActivityTimestamps = kwargs.pop('timestamps', {})
|
||||||
self.assets = kwargs.pop('assets', {})
|
self.assets: ActivityAssets = kwargs.pop('assets', {})
|
||||||
self.party = kwargs.pop('party', {})
|
self.party: ActivityParty = kwargs.pop('party', {})
|
||||||
self.application_id = _get_as_snowflake(kwargs, 'application_id')
|
self.application_id = _get_as_snowflake(kwargs, 'application_id')
|
||||||
self.name = kwargs.pop('name', None)
|
self.name = kwargs.pop('name', None)
|
||||||
self.url = kwargs.pop('url', None)
|
self.url = kwargs.pop('url', None)
|
||||||
self.flags = kwargs.pop('flags', 0)
|
self.flags = kwargs.pop('flags', 0)
|
||||||
self.sync_id = kwargs.pop('sync_id', None)
|
self.sync_id = kwargs.pop('sync_id', None)
|
||||||
self.session_id = kwargs.pop('session_id', None)
|
self.session_id = kwargs.pop('session_id', None)
|
||||||
|
self.buttons: List[ActivityButton] = kwargs.pop('buttons', [])
|
||||||
|
|
||||||
activity_type = 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)
|
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
|
||||||
@@ -269,6 +310,7 @@ class Activity(BaseActivity):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
|
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def large_image_text(self):
|
def large_image_text(self):
|
||||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
||||||
@@ -321,7 +363,7 @@ class Game(BaseActivity):
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
timestamps = extra['timestamps']
|
timestamps: ActivityTimestamps = extra['timestamps']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._start = 0
|
self._start = 0
|
||||||
self._end = 0
|
self._end = 0
|
||||||
@@ -365,11 +407,13 @@ class Game(BaseActivity):
|
|||||||
if self._end:
|
if self._end:
|
||||||
timestamps['end'] = self._end
|
timestamps['end'] = self._end
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
return {
|
return {
|
||||||
'type': ActivityType.playing.value,
|
'type': ActivityType.playing.value,
|
||||||
'name': str(self.name),
|
'name': str(self.name),
|
||||||
'timestamps': timestamps
|
'timestamps': timestamps
|
||||||
}
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, Game) and other.name == self.name
|
return isinstance(other, Game) and other.name == self.name
|
||||||
@@ -380,6 +424,7 @@ class Game(BaseActivity):
|
|||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.name)
|
return hash(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Streaming(BaseActivity):
|
class Streaming(BaseActivity):
|
||||||
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
||||||
|
|
||||||
@@ -433,8 +478,8 @@ class Streaming(BaseActivity):
|
|||||||
self.name = extra.pop('details', name)
|
self.name = extra.pop('details', name)
|
||||||
self.game = extra.pop('state', None)
|
self.game = extra.pop('state', None)
|
||||||
self.url = url
|
self.url = url
|
||||||
self.details = extra.pop('details', self.name) # compatibility
|
self.details = extra.pop('details', self.name) # compatibility
|
||||||
self.assets = extra.pop('assets', {})
|
self.assets: ActivityAssets = extra.pop('assets', {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
@@ -466,12 +511,14 @@ class Streaming(BaseActivity):
|
|||||||
return name[7:] if name[:7] == 'twitch:' else None
|
return name[7:] if name[:7] == 'twitch:' else None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
# fmt: off
|
||||||
ret = {
|
ret = {
|
||||||
'type': ActivityType.streaming.value,
|
'type': ActivityType.streaming.value,
|
||||||
'name': str(self.name),
|
'name': str(self.name),
|
||||||
'url': str(self.url),
|
'url': str(self.url),
|
||||||
'assets': self.assets
|
'assets': self.assets
|
||||||
}
|
}
|
||||||
|
# fmt: on
|
||||||
if self.details:
|
if self.details:
|
||||||
ret['details'] = self.details
|
ret['details'] = self.details
|
||||||
return ret
|
return ret
|
||||||
@@ -485,6 +532,7 @@ class Streaming(BaseActivity):
|
|||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.name)
|
return hash(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Spotify:
|
class Spotify:
|
||||||
"""Represents a Spotify listening activity from Discord. This is a special case of
|
"""Represents a Spotify listening activity from Discord. This is a special case of
|
||||||
:class:`Activity` that makes it easier to work with the Spotify integration.
|
:class:`Activity` that makes it easier to work with the Spotify integration.
|
||||||
@@ -508,8 +556,7 @@ class Spotify:
|
|||||||
Returns the string 'Spotify'.
|
Returns the string 'Spotify'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id',
|
__slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id', '_created_at')
|
||||||
'_created_at')
|
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
self._state = data.pop('state', None)
|
self._state = data.pop('state', None)
|
||||||
@@ -543,7 +590,7 @@ class Spotify:
|
|||||||
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
||||||
|
|
||||||
There is an alias for this named :attr:`color`"""
|
There is an alias for this named :attr:`color`"""
|
||||||
return Colour(0x1db954)
|
return Colour(0x1DB954)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self):
|
||||||
@@ -554,7 +601,7 @@ class Spotify:
|
|||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'flags': 48, # SYNC | PLAY
|
'flags': 48, # SYNC | PLAY
|
||||||
'name': 'Spotify',
|
'name': 'Spotify',
|
||||||
'assets': self._assets,
|
'assets': self._assets,
|
||||||
'party': self._party,
|
'party': self._party,
|
||||||
@@ -562,7 +609,7 @@ class Spotify:
|
|||||||
'session_id': self._session_id,
|
'session_id': self._session_id,
|
||||||
'timestamps': self._timestamps,
|
'timestamps': self._timestamps,
|
||||||
'details': self._details,
|
'details': self._details,
|
||||||
'state': self._state
|
'state': self._state,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -571,8 +618,12 @@ class Spotify:
|
|||||||
return 'Spotify'
|
return 'Spotify'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, Spotify) and other._session_id == self._session_id
|
return (
|
||||||
and other._sync_id == self._sync_id and other.start == self.start)
|
isinstance(other, Spotify)
|
||||||
|
and other._session_id == self._session_id
|
||||||
|
and other._sync_id == self._sync_id
|
||||||
|
and other.start == self.start
|
||||||
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@@ -584,7 +635,7 @@ class Spotify:
|
|||||||
return 'Spotify'
|
return 'Spotify'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
|
return f'<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
@@ -644,6 +695,7 @@ class Spotify:
|
|||||||
""":class:`str`: The party ID of the listening party."""
|
""":class:`str`: The party ID of the listening party."""
|
||||||
return self._party.get('id', '')
|
return self._party.get('id', '')
|
||||||
|
|
||||||
|
|
||||||
class CustomActivity(BaseActivity):
|
class CustomActivity(BaseActivity):
|
||||||
"""Represents a Custom activity from Discord.
|
"""Represents a Custom activity from Discord.
|
||||||
|
|
||||||
@@ -721,7 +773,7 @@ class CustomActivity(BaseActivity):
|
|||||||
return o
|
return o
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji)
|
return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@@ -738,7 +790,7 @@ class CustomActivity(BaseActivity):
|
|||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<CustomActivity name={0.name!r} emoji={0.emoji!r}>'.format(self)
|
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
|
||||||
|
|
||||||
|
|
||||||
def create_activity(data):
|
def create_activity(data):
|
||||||
|
|||||||
@@ -22,15 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .user import User
|
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .team import Team
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .guild import Guild
|
||||||
|
from .types.appinfo import (
|
||||||
|
AppInfo as AppInfoPayload,
|
||||||
|
PartialAppInfo as PartialAppInfoPayload,
|
||||||
|
Team as TeamPayload,
|
||||||
|
)
|
||||||
|
from .user import User
|
||||||
|
from .state import ConnectionState
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AppInfo',
|
'AppInfo',
|
||||||
|
'PartialAppInfo',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppInfo:
|
class AppInfo:
|
||||||
"""Represents the application info for the bot provided by Discord.
|
"""Represents the application info for the bot provided by Discord.
|
||||||
|
|
||||||
@@ -48,9 +62,7 @@ class AppInfo:
|
|||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
icon: Optional[:class:`str`]
|
description: :class:`str`
|
||||||
The icon hash, if it exists.
|
|
||||||
description: Optional[:class:`str`]
|
|
||||||
The application description.
|
The application description.
|
||||||
bot_public: :class:`bool`
|
bot_public: :class:`bool`
|
||||||
Whether the bot can be invited by anyone or if it is locked
|
Whether the bot can be invited by anyone or if it is locked
|
||||||
@@ -62,155 +74,174 @@ class AppInfo:
|
|||||||
A list of RPC origin URLs, if RPC is enabled.
|
A list of RPC origin URLs, if RPC is enabled.
|
||||||
summary: :class:`str`
|
summary: :class:`str`
|
||||||
If this application is a game sold on Discord,
|
If this application is a game sold on Discord,
|
||||||
this field will be the summary field for the store page of its primary SKU
|
this field will be the summary field for the store page of its primary SKU.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
verify_key: :class:`str`
|
verify_key: :class:`str`
|
||||||
The base64 encoded key for the GameSDK's GetTicket
|
The hex encoded key for verification in interactions and the
|
||||||
|
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
guild_id: Optional[:class:`int`]
|
guild_id: Optional[:class:`int`]
|
||||||
If this application is a game sold on Discord,
|
If this application is a game sold on Discord,
|
||||||
this field will be the guild to which it has been linked
|
this field will be the guild to which it has been linked to.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
primary_sku_id: Optional[:class:`int`]
|
primary_sku_id: Optional[:class:`int`]
|
||||||
If this application is a game sold on Discord,
|
If this application is a game sold on Discord,
|
||||||
this field will be the id of the "Game SKU" that is created, if exists
|
this field will be the id of the "Game SKU" that is created,
|
||||||
|
if it exists.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
slug: Optional[:class:`str`]
|
slug: Optional[:class:`str`]
|
||||||
If this application is a game sold on Discord,
|
If this application is a game sold on Discord,
|
||||||
this field will be the URL slug that links to the store page
|
this field will be the URL slug that links to the store page.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
cover_image: Optional[:class:`str`]
|
terms_of_service_url: Optional[:class:`str`]
|
||||||
If this application is a game sold on Discord,
|
The application's terms of service URL, if set.
|
||||||
this field will be the hash of the image on store embeds
|
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
privacy_policy_url: Optional[:class:`str`]
|
||||||
|
The application's privacy policy URL, if set.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins',
|
|
||||||
'bot_public', 'bot_require_code_grant', 'owner', 'icon',
|
|
||||||
'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id',
|
|
||||||
'slug', 'cover_image')
|
|
||||||
|
|
||||||
def __init__(self, state, data):
|
__slots__ = (
|
||||||
self._state = state
|
'_state',
|
||||||
|
'description',
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'rpc_origins',
|
||||||
|
'bot_public',
|
||||||
|
'bot_require_code_grant',
|
||||||
|
'owner',
|
||||||
|
'_icon',
|
||||||
|
'summary',
|
||||||
|
'verify_key',
|
||||||
|
'team',
|
||||||
|
'guild_id',
|
||||||
|
'primary_sku_id',
|
||||||
|
'slug',
|
||||||
|
'_cover_image',
|
||||||
|
'terms_of_service_url',
|
||||||
|
'privacy_policy_url',
|
||||||
|
)
|
||||||
|
|
||||||
self.id = int(data['id'])
|
def __init__(self, state: ConnectionState, data: AppInfoPayload):
|
||||||
self.name = data['name']
|
from .team import Team
|
||||||
self.description = data['description']
|
|
||||||
self.icon = data['icon']
|
|
||||||
self.rpc_origins = data['rpc_origins']
|
|
||||||
self.bot_public = data['bot_public']
|
|
||||||
self.bot_require_code_grant = data['bot_require_code_grant']
|
|
||||||
self.owner = User(state=self._state, data=data['owner'])
|
|
||||||
|
|
||||||
team = data.get('team')
|
self._state: ConnectionState = state
|
||||||
self.team = Team(state, team) if team else None
|
self.id: int = int(data['id'])
|
||||||
|
self.name: str = data['name']
|
||||||
|
self.description: str = data['description']
|
||||||
|
self._icon: Optional[str] = data['icon']
|
||||||
|
self.rpc_origins: List[str] = data['rpc_origins']
|
||||||
|
self.bot_public: bool = data['bot_public']
|
||||||
|
self.bot_require_code_grant: bool = data['bot_require_code_grant']
|
||||||
|
self.owner: User = state.store_user(data['owner'])
|
||||||
|
|
||||||
self.summary = data['summary']
|
team: Optional[TeamPayload] = data.get('team')
|
||||||
self.verify_key = data['verify_key']
|
self.team: Optional[Team] = Team(state, team) if team else None
|
||||||
|
|
||||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
self.summary: str = data['summary']
|
||||||
|
self.verify_key: str = data['verify_key']
|
||||||
|
|
||||||
self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id')
|
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
|
||||||
self.slug = data.get('slug')
|
|
||||||
self.cover_image = data.get('cover_image')
|
|
||||||
|
|
||||||
def __repr__(self):
|
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, 'primary_sku_id')
|
||||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} description={0.description!r} public={0.bot_public} ' \
|
self.slug: Optional[str] = data.get('slug')
|
||||||
'owner={0.owner!r}>'.format(self)
|
self._cover_image: Optional[str] = data.get('cover_image')
|
||||||
|
self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url')
|
||||||
|
self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url')
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'<{self.__class__.__name__} id={self.id} name={self.name!r} '
|
||||||
|
f'description={self.description!r} public={self.bot_public} '
|
||||||
|
f'owner={self.owner!r}>'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon_url(self):
|
def icon(self) -> Optional[Asset]:
|
||||||
""":class:`.Asset`: Retrieves the application's icon asset.
|
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||||
|
if self._icon is None:
|
||||||
This is equivalent to calling :meth:`icon_url_as` with
|
return None
|
||||||
the default parameters ('webp' format and a size of 1024).
|
return Asset._from_icon(self._state, self.id, self._icon, path='app')
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
|
||||||
"""
|
|
||||||
return self.icon_url_as()
|
|
||||||
|
|
||||||
def icon_url_as(self, *, format='webp', size=1024):
|
|
||||||
"""Returns an :class:`Asset` for the icon the application has.
|
|
||||||
|
|
||||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
|
||||||
The size must be a power of 2 between 16 and 4096.
|
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
format: :class:`str`
|
|
||||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_icon(self._state, self, 'app', format=format, size=size)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cover_image_url(self):
|
def cover_image(self) -> Optional[Asset]:
|
||||||
""":class:`.Asset`: Retrieves the cover image on a store embed.
|
"""Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any.
|
||||||
|
|
||||||
This is equivalent to calling :meth:`cover_image_url_as` with
|
This is only available if the application is a game sold on Discord.
|
||||||
the default parameters ('webp' format and a size of 1024).
|
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
|
||||||
"""
|
"""
|
||||||
return self.cover_image_url_as()
|
if self._cover_image is None:
|
||||||
|
return None
|
||||||
def cover_image_url_as(self, *, format='webp', size=1024):
|
return Asset._from_cover_image(self._state, self.id, self._cover_image)
|
||||||
"""Returns an :class:`Asset` for the image on store embeds
|
|
||||||
if this application is a game sold on Discord.
|
|
||||||
|
|
||||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
|
||||||
The size must be a power of 2 between 16 and 4096.
|
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
format: :class:`str`
|
|
||||||
The format to attempt to convert the image to. Defaults to 'webp'.
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_cover_image(self._state, self, format=format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def guild(self):
|
def guild(self) -> Optional[Guild]:
|
||||||
"""Optional[:class:`Guild`]: If this application is a game sold on Discord,
|
"""Optional[:class:`Guild`]: If this application is a game sold on Discord,
|
||||||
this field will be the guild to which it has been linked
|
this field will be the guild to which it has been linked
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
"""
|
"""
|
||||||
return self._state._get_guild(int(self.guild_id))
|
return self._state._get_guild(self.guild_id)
|
||||||
|
|
||||||
|
class PartialAppInfo:
|
||||||
|
"""Represents a partial AppInfo given by :func:`~GuildChannel.create_invite`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-------------
|
||||||
|
id: :class:`int`
|
||||||
|
The application ID.
|
||||||
|
name: :class:`str`
|
||||||
|
The application name.
|
||||||
|
description: :class:`str`
|
||||||
|
The application description.
|
||||||
|
rpc_origins: Optional[List[:class:`str`]]
|
||||||
|
A list of RPC origin URLs, if RPC is enabled.
|
||||||
|
summary: :class:`str`
|
||||||
|
If this application is a game sold on Discord,
|
||||||
|
this field will be the summary field for the store page of its primary SKU.
|
||||||
|
verify_key: :class:`str`
|
||||||
|
The hex encoded key for verification in interactions and the
|
||||||
|
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
|
||||||
|
terms_of_service_url: Optional[:class:`str`]
|
||||||
|
The application's terms of service URL, if set.
|
||||||
|
privacy_policy_url: Optional[:class:`str`]
|
||||||
|
The application's privacy policy URL, if set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('_state', 'id', 'name', 'description', 'rpc_origins', 'summary', 'verify_key', 'terms_of_service_url', 'privacy_policy_url', '_icon')
|
||||||
|
|
||||||
|
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
|
||||||
|
self._state: ConnectionState = state
|
||||||
|
self.id: int = int(data['id'])
|
||||||
|
self.name: str = data['name']
|
||||||
|
self._icon: Optional[str] = data.get('icon')
|
||||||
|
self.description: str = data['description']
|
||||||
|
self.rpc_origins: Optional[List[str]] = data.get('rpc_origins')
|
||||||
|
self.summary: str = data['summary']
|
||||||
|
self.verify_key: str = data['verify_key']
|
||||||
|
self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url')
|
||||||
|
self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url')
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> Optional[Asset]:
|
||||||
|
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||||
|
if self._icon is None:
|
||||||
|
return None
|
||||||
|
return Asset._from_icon(self._state, self.id, self._icon, path='app')
|
||||||
|
|||||||
454
discord/asset.py
454
discord/asset.py
@@ -22,19 +22,101 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
from typing import Any, Literal, Optional, TYPE_CHECKING, Tuple, Union
|
||||||
from .errors import DiscordException
|
from .errors import DiscordException
|
||||||
from .errors import InvalidArgument
|
from .errors import InvalidArgument
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
import yarl
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Asset',
|
'Asset',
|
||||||
)
|
)
|
||||||
|
|
||||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
if TYPE_CHECKING:
|
||||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png']
|
||||||
|
ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif']
|
||||||
|
|
||||||
class Asset:
|
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||||
|
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||||
|
|
||||||
|
|
||||||
|
MISSING = utils.MISSING
|
||||||
|
|
||||||
|
class AssetMixin:
|
||||||
|
url: str
|
||||||
|
_state: Optional[Any]
|
||||||
|
|
||||||
|
async def read(self) -> bytes:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Retrieves the content of this asset as a :class:`bytes` object.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
DiscordException
|
||||||
|
There was no internal connection state.
|
||||||
|
HTTPException
|
||||||
|
Downloading the asset failed.
|
||||||
|
NotFound
|
||||||
|
The asset was deleted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`bytes`
|
||||||
|
The content of the asset.
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise DiscordException('Invalid state (no ConnectionState provided)')
|
||||||
|
|
||||||
|
return await self._state.http.get_from_cdn(self.url)
|
||||||
|
|
||||||
|
async def save(self, fp: Union[str, bytes, os.PathLike, io.BufferedIOBase], *, seek_begin: bool = True) -> int:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Saves this asset into a file-like object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
|
||||||
|
The file-like object to save this attachment to or the filename
|
||||||
|
to use. If a filename is passed then a file is created with that
|
||||||
|
filename and used instead.
|
||||||
|
seek_begin: :class:`bool`
|
||||||
|
Whether to seek to the beginning of the file after saving is
|
||||||
|
successfully done.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
DiscordException
|
||||||
|
There was no internal connection state.
|
||||||
|
HTTPException
|
||||||
|
Downloading the asset failed.
|
||||||
|
NotFound
|
||||||
|
The asset was deleted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`int`
|
||||||
|
The number of bytes written.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = await self.read()
|
||||||
|
if isinstance(fp, io.BufferedIOBase):
|
||||||
|
written = fp.write(data)
|
||||||
|
if seek_begin:
|
||||||
|
fp.seek(0)
|
||||||
|
return written
|
||||||
|
else:
|
||||||
|
with open(fp, 'wb') as f:
|
||||||
|
return f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(AssetMixin):
|
||||||
"""Represents a CDN asset on Discord.
|
"""Represents a CDN asset on Discord.
|
||||||
|
|
||||||
.. container:: operations
|
.. container:: operations
|
||||||
@@ -47,10 +129,6 @@ class Asset:
|
|||||||
|
|
||||||
Returns the length of the CDN asset's URL.
|
Returns the length of the CDN asset's URL.
|
||||||
|
|
||||||
.. describe:: bool(x)
|
|
||||||
|
|
||||||
Checks if the Asset has a URL.
|
|
||||||
|
|
||||||
.. describe:: x == y
|
.. describe:: x == y
|
||||||
|
|
||||||
Checks if the asset is equal to another asset.
|
Checks if the asset is equal to another asset.
|
||||||
@@ -63,202 +141,252 @@ class Asset:
|
|||||||
|
|
||||||
Returns the hash of the asset.
|
Returns the hash of the asset.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_state', '_url')
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
|
'_state',
|
||||||
|
'_url',
|
||||||
|
'_animated',
|
||||||
|
'_key',
|
||||||
|
)
|
||||||
|
|
||||||
BASE = 'https://cdn.discordapp.com'
|
BASE = 'https://cdn.discordapp.com'
|
||||||
|
|
||||||
def __init__(self, state, url=None):
|
def __init__(self, state, *, url: str, key: str, animated: bool = False):
|
||||||
self._state = state
|
self._state = state
|
||||||
self._url = url
|
self._url = url
|
||||||
|
self._animated = animated
|
||||||
|
self._key = key
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024):
|
def _from_default_avatar(cls, state, index: int) -> Asset:
|
||||||
if not utils.valid_icon_size(size):
|
return cls(
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
state,
|
||||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
url=f'{cls.BASE}/embed/avatars/{index}.png',
|
||||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
key=str(index),
|
||||||
if format == "gif" and not user.is_avatar_animated():
|
animated=False,
|
||||||
raise InvalidArgument("non animated avatars do not support gif format")
|
)
|
||||||
if static_format not in 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
|
|
||||||
|
|
||||||
if format is None:
|
|
||||||
format = 'gif' if user.is_avatar_animated() else static_format
|
|
||||||
|
|
||||||
return cls(state, '/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_icon(cls, state, object, path, *, format='webp', size=1024):
|
def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
|
||||||
if object.icon is None:
|
animated = avatar.startswith('a_')
|
||||||
return cls(state)
|
format = 'gif' if animated else 'png'
|
||||||
|
return cls(
|
||||||
if not utils.valid_icon_size(size):
|
state,
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
url=f'{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024',
|
||||||
if format not in VALID_STATIC_FORMATS:
|
key=avatar,
|
||||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
animated=animated,
|
||||||
|
)
|
||||||
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
|
|
||||||
return cls(state, url)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_cover_image(cls, state, obj, *, format='webp', size=1024):
|
def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
|
||||||
if obj.cover_image is None:
|
return cls(
|
||||||
return cls(state)
|
state,
|
||||||
|
url=f'{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024',
|
||||||
if not utils.valid_icon_size(size):
|
key=icon_hash,
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
animated=False,
|
||||||
if format not in 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)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024):
|
def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset:
|
||||||
if not utils.valid_icon_size(size):
|
return cls(
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
state,
|
||||||
if format not in VALID_STATIC_FORMATS:
|
url=f'{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024',
|
||||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
key=cover_image_hash,
|
||||||
|
animated=False,
|
||||||
if hash is None:
|
)
|
||||||
return cls(state)
|
|
||||||
|
|
||||||
url = '/{key}/{0}/{1}.{2}?size={3}'
|
|
||||||
return cls(state, url.format(id, hash, format, size, key=key))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024):
|
def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
|
||||||
if not utils.valid_icon_size(size):
|
return cls(
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
state,
|
||||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
url=f'{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024',
|
||||||
raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
|
key=image,
|
||||||
if format == "gif" and not guild.is_icon_animated():
|
animated=False,
|
||||||
raise InvalidArgument("non animated guild icons do not support gif format")
|
)
|
||||||
if static_format not in VALID_STATIC_FORMATS:
|
|
||||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
|
||||||
|
|
||||||
if guild.icon is None:
|
|
||||||
return cls(state)
|
|
||||||
|
|
||||||
if format is None:
|
|
||||||
format = 'gif' if guild.is_icon_animated() else static_format
|
|
||||||
|
|
||||||
return cls(state, '/icons/{0.id}/{0.icon}.{1}?size={2}'.format(guild, format, size))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_sticker_url(cls, state, sticker, *, size=1024):
|
def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
|
||||||
if not utils.valid_icon_size(size):
|
animated = icon_hash.startswith('a_')
|
||||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
format = 'gif' if animated else 'png'
|
||||||
|
return cls(
|
||||||
return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size))
|
state,
|
||||||
|
url=f'{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024',
|
||||||
|
key=icon_hash,
|
||||||
|
animated=animated,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
|
def _from_sticker(cls, state, sticker_id: int, sticker_hash: str) -> Asset:
|
||||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
return cls(
|
||||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
state,
|
||||||
if format == "gif" and not emoji.animated:
|
url=f'{cls.BASE}/stickers/{sticker_id}/{sticker_hash}.png?size=1024',
|
||||||
raise InvalidArgument("non animated emoji's do not support gif format")
|
key=sticker_hash,
|
||||||
if static_format not in VALID_STATIC_FORMATS:
|
animated=False,
|
||||||
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, f'/emojis/{emoji.id}.{format}')
|
def __str__(self) -> str:
|
||||||
|
return self._url
|
||||||
|
|
||||||
def __str__(self):
|
def __len__(self) -> int:
|
||||||
return self.BASE + self._url if self._url is not None else ''
|
return len(self._url)
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
if self._url:
|
|
||||||
return len(self.BASE + self._url)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return self._url is not None
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Asset url={self._url!r}>'
|
shorten = self._url.replace(self.BASE, '')
|
||||||
|
return f'<Asset url={shorten!r}>'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, Asset) and self._url == other._url
|
return isinstance(other, Asset) and self._url == other._url
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self._url)
|
return hash(self._url)
|
||||||
|
|
||||||
async def read(self):
|
@property
|
||||||
"""|coro|
|
def url(self) -> str:
|
||||||
|
""":class:`str`: Returns the underlying URL of the asset."""
|
||||||
|
return self._url
|
||||||
|
|
||||||
Retrieves the content of this asset as a :class:`bytes` object.
|
@property
|
||||||
|
def key(self) -> str:
|
||||||
|
""":class:`str`: Returns the identifying key of the asset."""
|
||||||
|
return self._key
|
||||||
|
|
||||||
.. warning::
|
def is_animated(self) -> bool:
|
||||||
|
""":class:`bool`: Returns whether the asset is animated."""
|
||||||
|
return self._animated
|
||||||
|
|
||||||
:class:`PartialEmoji` won't have a connection state if user created,
|
def replace(
|
||||||
and a URL won't be present if a custom image isn't associated with
|
self,
|
||||||
the asset, e.g. a guild with no custom icon.
|
size: int = MISSING,
|
||||||
|
format: ValidAssetFormatTypes = MISSING,
|
||||||
.. versionadded:: 1.1
|
static_format: ValidStaticFormatTypes = MISSING,
|
||||||
|
) -> Asset:
|
||||||
Raises
|
"""Returns a new asset with the passed components replaced.
|
||||||
------
|
|
||||||
DiscordException
|
|
||||||
There was no valid URL or internal connection state.
|
|
||||||
HTTPException
|
|
||||||
Downloading the asset failed.
|
|
||||||
NotFound
|
|
||||||
The asset was deleted.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`bytes`
|
|
||||||
The content of the asset.
|
|
||||||
"""
|
|
||||||
if not self._url:
|
|
||||||
raise DiscordException('Invalid asset (no URL provided)')
|
|
||||||
|
|
||||||
if self._state is None:
|
|
||||||
raise DiscordException('Invalid state (no ConnectionState provided)')
|
|
||||||
|
|
||||||
return await self._state.http.get_from_cdn(self.BASE + self._url)
|
|
||||||
|
|
||||||
async def save(self, fp, *, seek_begin=True):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Saves this asset into a file-like object.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
-----------
|
||||||
fp: Union[BinaryIO, :class:`os.PathLike`]
|
size: :class:`int`
|
||||||
Same as in :meth:`Attachment.save`.
|
The new size of the asset.
|
||||||
seek_begin: :class:`bool`
|
format: :class:`str`
|
||||||
Same as in :meth:`Attachment.save`.
|
The new format to change it to. Must be either
|
||||||
|
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
|
||||||
|
static_format: :class:`str`
|
||||||
|
The new format to change it to if the asset isn't animated.
|
||||||
|
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
-------
|
||||||
DiscordException
|
InvalidArgument
|
||||||
There was no valid URL or internal connection state.
|
An invalid size or format was passed.
|
||||||
HTTPException
|
|
||||||
Downloading the asset failed.
|
|
||||||
NotFound
|
|
||||||
The asset was deleted.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
--------
|
--------
|
||||||
:class:`int`
|
:class:`Asset`
|
||||||
The number of bytes written.
|
The newly updated asset.
|
||||||
|
"""
|
||||||
|
url = yarl.URL(self._url)
|
||||||
|
path, _ = os.path.splitext(url.path)
|
||||||
|
|
||||||
|
if format is not MISSING:
|
||||||
|
if self._animated:
|
||||||
|
if format not in VALID_ASSET_FORMATS:
|
||||||
|
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||||
|
else:
|
||||||
|
if format not in VALID_STATIC_FORMATS:
|
||||||
|
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
|
||||||
|
url = url.with_path(f'{path}.{format}')
|
||||||
|
|
||||||
|
if static_format is not MISSING and not self._animated:
|
||||||
|
if static_format not in VALID_STATIC_FORMATS:
|
||||||
|
raise InvalidArgument(f'static_format must be one of {VALID_STATIC_FORMATS}')
|
||||||
|
url = url.with_path(f'{path}.{static_format}')
|
||||||
|
|
||||||
|
if size is not MISSING:
|
||||||
|
if not utils.valid_icon_size(size):
|
||||||
|
raise InvalidArgument('size must be a power of 2 between 16 and 4096')
|
||||||
|
url = url.with_query(size=size)
|
||||||
|
else:
|
||||||
|
url = url.with_query(url.raw_query_string)
|
||||||
|
|
||||||
|
url = str(url)
|
||||||
|
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||||
|
|
||||||
|
def with_size(self, size: int) -> Asset:
|
||||||
|
"""Returns a new asset with the specified size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
size: :class:`int`
|
||||||
|
The new size of the asset.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
InvalidArgument
|
||||||
|
The asset had an invalid size.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Asset`
|
||||||
|
The new updated asset.
|
||||||
|
"""
|
||||||
|
if not utils.valid_icon_size(size):
|
||||||
|
raise InvalidArgument('size must be a power of 2 between 16 and 4096')
|
||||||
|
|
||||||
|
url = str(yarl.URL(self._url).with_query(size=size))
|
||||||
|
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||||
|
|
||||||
|
def with_format(self, format: ValidAssetFormatTypes) -> Asset:
|
||||||
|
"""Returns a new asset with the specified format.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
format: :class:`str`
|
||||||
|
The new format of the asset.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
InvalidArgument
|
||||||
|
The asset had an invalid format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Asset`
|
||||||
|
The new updated asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = await self.read()
|
if self._animated:
|
||||||
if isinstance(fp, io.IOBase) and fp.writable():
|
if format not in VALID_ASSET_FORMATS:
|
||||||
written = fp.write(data)
|
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||||
if seek_begin:
|
|
||||||
fp.seek(0)
|
|
||||||
return written
|
|
||||||
else:
|
else:
|
||||||
with open(fp, 'wb') as f:
|
if format not in VALID_STATIC_FORMATS:
|
||||||
return f.write(data)
|
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
|
||||||
|
|
||||||
|
url = yarl.URL(self._url)
|
||||||
|
path, _ = os.path.splitext(url.path)
|
||||||
|
url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string))
|
||||||
|
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||||
|
|
||||||
|
def with_static_format(self, format: ValidStaticFormatTypes) -> Asset:
|
||||||
|
"""Returns a new asset with the specified static format.
|
||||||
|
|
||||||
|
This only changes the format if the underlying asset is
|
||||||
|
not animated. Otherwise, the asset is not changed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
format: :class:`str`
|
||||||
|
The new static format of the asset.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
InvalidArgument
|
||||||
|
The asset had an invalid format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`Asset`
|
||||||
|
The new updated asset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._animated:
|
||||||
|
return self
|
||||||
|
return self.with_format(format)
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import utils, enums
|
from __future__ import annotations
|
||||||
from .object import Object
|
|
||||||
from .permissions import PermissionOverwrite, Permissions
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
|
from . import enums, utils
|
||||||
|
from .asset import Asset
|
||||||
from .colour import Colour
|
from .colour import Colour
|
||||||
from .invite import Invite
|
from .invite import Invite
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
|
from .object import Object
|
||||||
|
from .permissions import PermissionOverwrite, Permissions
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AuditLogDiff',
|
'AuditLogDiff',
|
||||||
@@ -35,40 +40,56 @@ __all__ = (
|
|||||||
'AuditLogEntry',
|
'AuditLogEntry',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _transform_verification_level(entry, data):
|
|
||||||
return enums.try_enum(enums.VerificationLevel, data)
|
|
||||||
|
|
||||||
def _transform_default_notifications(entry, data):
|
if TYPE_CHECKING:
|
||||||
return enums.try_enum(enums.NotificationLevel, data)
|
import datetime
|
||||||
|
|
||||||
def _transform_explicit_content_filter(entry, data):
|
from . import abc
|
||||||
return enums.try_enum(enums.ContentFilter, data)
|
from .emoji import Emoji
|
||||||
|
from .guild import Guild
|
||||||
|
from .member import Member
|
||||||
|
from .role import Role
|
||||||
|
from .types.audit_log import AuditLogChange as AuditLogChangePayload
|
||||||
|
from .types.audit_log import AuditLogEntry as AuditLogEntryPayload
|
||||||
|
from .types.channel import PermissionOverwrite as PermissionOverwritePayload
|
||||||
|
from .types.role import Role as RolePayload
|
||||||
|
from .types.snowflake import Snowflake
|
||||||
|
from .user import User
|
||||||
|
|
||||||
def _transform_permissions(entry, data):
|
|
||||||
return Permissions(data)
|
|
||||||
|
|
||||||
def _transform_color(entry, data):
|
def _transform_permissions(entry: AuditLogEntry, data: str) -> Permissions:
|
||||||
|
return Permissions(int(data))
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_color(entry: AuditLogEntry, data: int) -> Colour:
|
||||||
return Colour(data)
|
return Colour(data)
|
||||||
|
|
||||||
def _transform_snowflake(entry, data):
|
|
||||||
|
def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int:
|
||||||
return int(data)
|
return int(data)
|
||||||
|
|
||||||
def _transform_channel(entry, data):
|
|
||||||
|
def _transform_channel(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Object]:
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||||
|
|
||||||
def _transform_owner_id(entry, data):
|
|
||||||
|
def _transform_owner_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
return entry._get_member(int(data))
|
return entry._get_member(int(data))
|
||||||
|
|
||||||
def _transform_inviter_id(entry, data):
|
|
||||||
|
def _transform_inviter_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
return entry._get_member(int(data))
|
return entry._get_member(int(data))
|
||||||
|
|
||||||
def _transform_overwrites(entry, data):
|
|
||||||
|
def _transform_overwrites(
|
||||||
|
entry: AuditLogEntry, data: List[PermissionOverwritePayload]
|
||||||
|
) -> List[Tuple[Object, PermissionOverwrite]]:
|
||||||
overwrites = []
|
overwrites = []
|
||||||
for elem in data:
|
for elem in data:
|
||||||
allow = Permissions(elem['allow'])
|
allow = Permissions(elem['allow'])
|
||||||
@@ -77,9 +98,10 @@ def _transform_overwrites(entry, data):
|
|||||||
|
|
||||||
ow_type = elem['type']
|
ow_type = elem['type']
|
||||||
ow_id = int(elem['id'])
|
ow_id = int(elem['id'])
|
||||||
if ow_type == 'role':
|
target = None
|
||||||
|
if ow_type == '0':
|
||||||
target = entry.guild.get_role(ow_id)
|
target = entry.guild.get_role(ow_id)
|
||||||
else:
|
elif ow_type == '1':
|
||||||
target = entry._get_member(ow_id)
|
target = entry._get_member(ow_id)
|
||||||
|
|
||||||
if target is None:
|
if target is None:
|
||||||
@@ -89,21 +111,66 @@ def _transform_overwrites(entry, data):
|
|||||||
|
|
||||||
return overwrites
|
return overwrites
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_icon(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_avatar(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]], Optional[Asset]]:
|
||||||
|
def _transform(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
|
||||||
|
|
||||||
|
return _transform
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T', bound=enums.Enum)
|
||||||
|
|
||||||
|
|
||||||
|
def _enum_transformer(enum: Type[T]) -> Callable[[AuditLogEntry, int], T]:
|
||||||
|
def _transform(entry: AuditLogEntry, data: int) -> T:
|
||||||
|
return enums.try_enum(enum, data)
|
||||||
|
|
||||||
|
return _transform
|
||||||
|
|
||||||
|
|
||||||
class AuditLogDiff:
|
class AuditLogDiff:
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self.__dict__)
|
return len(self.__dict__)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
||||||
return iter(self.__dict__.items())
|
yield from self.__dict__.items()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
||||||
return f'<AuditLogDiff {values}>'
|
return f'<AuditLogDiff {values}>'
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
...
|
||||||
|
|
||||||
|
def __setattr__(self, key: str, value: Any) -> Any:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
Transformer = Callable[["AuditLogEntry", Any], Any]
|
||||||
|
|
||||||
|
|
||||||
class AuditLogChanges:
|
class AuditLogChanges:
|
||||||
TRANSFORMERS = {
|
# fmt: off
|
||||||
'verification_level': (None, _transform_verification_level),
|
TRANSFORMERS: ClassVar[Dict[str, Tuple[Optional[str], Optional[Transformer]]]] = {
|
||||||
'explicit_content_filter': (None, _transform_explicit_content_filter),
|
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
|
||||||
|
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
|
||||||
'allow': (None, _transform_permissions),
|
'allow': (None, _transform_permissions),
|
||||||
'deny': (None, _transform_permissions),
|
'deny': (None, _transform_permissions),
|
||||||
'permissions': (None, _transform_permissions),
|
'permissions': (None, _transform_permissions),
|
||||||
@@ -115,15 +182,25 @@ class AuditLogChanges:
|
|||||||
'afk_channel_id': ('afk_channel', _transform_channel),
|
'afk_channel_id': ('afk_channel', _transform_channel),
|
||||||
'system_channel_id': ('system_channel', _transform_channel),
|
'system_channel_id': ('system_channel', _transform_channel),
|
||||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||||
|
'rules_channel_id': ('rules_channel', _transform_channel),
|
||||||
|
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
|
||||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||||
'splash_hash': ('splash', None),
|
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
|
||||||
'icon_hash': ('icon', None),
|
'banner_hash': ('banner', _guild_hash_transformer('banners')),
|
||||||
'avatar_hash': ('avatar', None),
|
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
|
||||||
|
'icon_hash': ('icon', _transform_icon),
|
||||||
|
'avatar_hash': ('avatar', _transform_avatar),
|
||||||
'rate_limit_per_user': ('slowmode_delay', None),
|
'rate_limit_per_user': ('slowmode_delay', None),
|
||||||
'default_message_notifications': ('default_notifications', _transform_default_notifications),
|
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
|
||||||
|
'region': (None, _enum_transformer(enums.VoiceRegion)),
|
||||||
|
'rtc_region': (None, _enum_transformer(enums.VoiceRegion)),
|
||||||
|
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
|
||||||
|
'privacy_level': (None, _enum_transformer(enums.StagePrivacyLevel)),
|
||||||
|
'type': (None, _enum_transformer(enums.ChannelType)),
|
||||||
}
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
def __init__(self, entry, data):
|
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]):
|
||||||
self.before = AuditLogDiff()
|
self.before = AuditLogDiff()
|
||||||
self.after = AuditLogDiff()
|
self.after = AuditLogDiff()
|
||||||
|
|
||||||
@@ -132,18 +209,22 @@ class AuditLogChanges:
|
|||||||
|
|
||||||
# special cases for role add/remove
|
# special cases for role add/remove
|
||||||
if attr == '$add':
|
if attr == '$add':
|
||||||
self._handle_role(self.before, self.after, entry, elem['new_value'])
|
self._handle_role(self.before, self.after, entry, elem['new_value']) # type: ignore
|
||||||
continue
|
continue
|
||||||
elif attr == '$remove':
|
elif attr == '$remove':
|
||||||
self._handle_role(self.after, self.before, entry, elem['new_value'])
|
self._handle_role(self.after, self.before, entry, elem['new_value']) # type: ignore
|
||||||
continue
|
continue
|
||||||
|
|
||||||
transformer = self.TRANSFORMERS.get(attr)
|
try:
|
||||||
if transformer:
|
key, transformer = self.TRANSFORMERS[attr]
|
||||||
key, transformer = transformer
|
except (ValueError, KeyError):
|
||||||
|
transformer = None
|
||||||
|
else:
|
||||||
if key:
|
if key:
|
||||||
attr = key
|
attr = key
|
||||||
|
|
||||||
|
transformer: Optional[Transformer]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
before = elem['old_value']
|
before = elem['old_value']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -168,16 +249,19 @@ class AuditLogChanges:
|
|||||||
if hasattr(self.after, 'colour'):
|
if hasattr(self.after, 'colour'):
|
||||||
self.after.color = self.after.colour
|
self.after.color = self.after.colour
|
||||||
self.before.color = self.before.colour
|
self.before.color = self.before.colour
|
||||||
|
if hasattr(self.after, 'expire_behavior'):
|
||||||
|
self.after.expire_behaviour = self.after.expire_behavior
|
||||||
|
self.before.expire_behaviour = self.before.expire_behavior
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
|
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
|
||||||
|
|
||||||
def _handle_role(self, first, second, entry, elem):
|
def _handle_role(self, first: AuditLogDiff, second: AuditLogDiff, entry: AuditLogEntry, elem: List[RolePayload]) -> None:
|
||||||
if not hasattr(first, 'roles'):
|
if not hasattr(first, 'roles'):
|
||||||
setattr(first, 'roles', [])
|
setattr(first, 'roles', [])
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
g = entry.guild
|
g: Guild = entry.guild # type: ignore
|
||||||
|
|
||||||
for e in elem:
|
for e in elem:
|
||||||
role_id = int(e['id'])
|
role_id = int(e['id'])
|
||||||
@@ -185,12 +269,36 @@ class AuditLogChanges:
|
|||||||
|
|
||||||
if role is None:
|
if role is None:
|
||||||
role = Object(id=role_id)
|
role = Object(id=role_id)
|
||||||
role.name = e['name']
|
role.name = e['name'] # type: ignore
|
||||||
|
|
||||||
data.append(role)
|
data.append(role)
|
||||||
|
|
||||||
setattr(second, 'roles', data)
|
setattr(second, 'roles', data)
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogProxyMemberPrune:
|
||||||
|
delete_member_days: int
|
||||||
|
members_removed: int
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogProxyMemberMoveOrMessageDelete:
|
||||||
|
channel: abc.GuildChannel
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogProxyMemberDisconnect:
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogProxyPinAction:
|
||||||
|
channel: abc.GuildChannel
|
||||||
|
message_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogProxyStageInstanceAction:
|
||||||
|
channel: abc.GuildChannel
|
||||||
|
|
||||||
|
|
||||||
class AuditLogEntry(Hashable):
|
class AuditLogEntry(Hashable):
|
||||||
r"""Represents an Audit Log entry.
|
r"""Represents an Audit Log entry.
|
||||||
|
|
||||||
@@ -234,13 +342,13 @@ class AuditLogEntry(Hashable):
|
|||||||
which actions have this field filled out.
|
which actions have this field filled out.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, users, data, guild):
|
def __init__(self, *, users: Dict[int, User], data: AuditLogEntryPayload, guild: Guild):
|
||||||
self._state = guild._state
|
self._state = guild._state
|
||||||
self.guild = guild
|
self.guild = guild
|
||||||
self._users = users
|
self._users = users
|
||||||
self._from_data(data)
|
self._from_data(data)
|
||||||
|
|
||||||
def _from_data(self, data):
|
def _from_data(self, data: AuditLogEntryPayload) -> None:
|
||||||
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
|
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
|
||||||
self.id = int(data['id'])
|
self.id = int(data['id'])
|
||||||
|
|
||||||
@@ -251,41 +359,58 @@ class AuditLogEntry(Hashable):
|
|||||||
if isinstance(self.action, enums.AuditLogAction) and self.extra:
|
if isinstance(self.action, enums.AuditLogAction) and self.extra:
|
||||||
if self.action is enums.AuditLogAction.member_prune:
|
if self.action is enums.AuditLogAction.member_prune:
|
||||||
# member prune has two keys with useful information
|
# member prune has two keys with useful information
|
||||||
self.extra = type('_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()})()
|
self.extra: _AuditLogProxyMemberPrune = type(
|
||||||
|
'_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()}
|
||||||
|
)()
|
||||||
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
|
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
|
||||||
channel_id = int(self.extra['channel_id'])
|
channel_id = int(self.extra['channel_id'])
|
||||||
elems = {
|
elems = {
|
||||||
'count': int(self.extra['count']),
|
'count': int(self.extra['count']),
|
||||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id)
|
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||||
}
|
}
|
||||||
self.extra = type('_AuditLogProxy', (), elems)()
|
self.extra: _AuditLogProxyMemberMoveOrMessageDelete = type('_AuditLogProxy', (), elems)()
|
||||||
elif self.action is enums.AuditLogAction.member_disconnect:
|
elif self.action is enums.AuditLogAction.member_disconnect:
|
||||||
# The member disconnect action has a dict with some information
|
# The member disconnect action has a dict with some information
|
||||||
elems = {
|
elems = {
|
||||||
'count': int(self.extra['count']),
|
'count': int(self.extra['count']),
|
||||||
}
|
}
|
||||||
self.extra = type('_AuditLogProxy', (), elems)()
|
self.extra: _AuditLogProxyMemberDisconnect = type('_AuditLogProxy', (), elems)()
|
||||||
elif self.action.name.endswith('pin'):
|
elif self.action.name.endswith('pin'):
|
||||||
# the pin actions have a dict with some information
|
# the pin actions have a dict with some information
|
||||||
channel_id = int(self.extra['channel_id'])
|
channel_id = int(self.extra['channel_id'])
|
||||||
message_id = int(self.extra['message_id'])
|
|
||||||
elems = {
|
elems = {
|
||||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
|
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||||
'message_id': message_id
|
'message_id': int(self.extra['message_id']),
|
||||||
}
|
}
|
||||||
self.extra = type('_AuditLogProxy', (), elems)()
|
self.extra: _AuditLogProxyPinAction = type('_AuditLogProxy', (), elems)()
|
||||||
elif self.action.name.startswith('overwrite_'):
|
elif self.action.name.startswith('overwrite_'):
|
||||||
# the overwrite_ actions have a dict with some information
|
# the overwrite_ actions have a dict with some information
|
||||||
instance_id = int(self.extra['id'])
|
instance_id = int(self.extra['id'])
|
||||||
the_type = self.extra.get('type')
|
the_type = self.extra.get('type')
|
||||||
if the_type == 'member':
|
if the_type == '1':
|
||||||
self.extra = self._get_member(instance_id)
|
self.extra = self._get_member(instance_id)
|
||||||
else:
|
elif the_type == '0':
|
||||||
role = self.guild.get_role(instance_id)
|
role = self.guild.get_role(instance_id)
|
||||||
if role is None:
|
if role is None:
|
||||||
role = Object(id=instance_id)
|
role = Object(id=instance_id)
|
||||||
role.name = self.extra.get('role_name')
|
role.name = self.extra.get('role_name') # type: ignore
|
||||||
self.extra = role
|
self.extra: Role = role
|
||||||
|
elif self.action.name.startswith('stage_instance'):
|
||||||
|
channel_id = int(self.extra['channel_id'])
|
||||||
|
elems = {'channel': self.guild.get_channel(channel_id) or Object(id=channel_id)}
|
||||||
|
self.extra: _AuditLogProxyStageInstanceAction = type('_AuditLogProxy', (), elems)()
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
self.extra: Union[
|
||||||
|
_AuditLogProxyMemberPrune,
|
||||||
|
_AuditLogProxyMemberMoveOrMessageDelete,
|
||||||
|
_AuditLogProxyMemberDisconnect,
|
||||||
|
_AuditLogProxyPinAction,
|
||||||
|
_AuditLogProxyStageInstanceAction,
|
||||||
|
Member, User, None,
|
||||||
|
Role,
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
# this key is not present when the above is present, typically.
|
# this key is not present when the above is present, typically.
|
||||||
# It's a list of { new_value: a, old_value: b, key: c }
|
# It's a list of { new_value: a, old_value: b, key: c }
|
||||||
@@ -294,22 +419,22 @@ class AuditLogEntry(Hashable):
|
|||||||
# into meaningful data when requested
|
# into meaningful data when requested
|
||||||
self._changes = data.get('changes', [])
|
self._changes = data.get('changes', [])
|
||||||
|
|
||||||
self.user = self._get_member(utils._get_as_snowflake(data, 'user_id'))
|
self.user = self._get_member(utils._get_as_snowflake(data, 'user_id')) # type: ignore
|
||||||
self._target_id = utils._get_as_snowflake(data, 'target_id')
|
self._target_id = utils._get_as_snowflake(data, 'target_id')
|
||||||
|
|
||||||
def _get_member(self, user_id):
|
def _get_member(self, user_id: int) -> Union[Member, User, None]:
|
||||||
return self.guild.get_member(user_id) or self._users.get(user_id)
|
return self.guild.get_member(user_id) or self._users.get(user_id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>'.format(self)
|
return f'<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>'
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
|
||||||
return utils.snowflake_time(self.id)
|
return utils.snowflake_time(self.id)
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def target(self):
|
def target(self) -> Union[Guild, abc.GuildChannel, Member, User, Role, Invite, Emoji, Object, None]:
|
||||||
try:
|
try:
|
||||||
converter = getattr(self, '_convert_target_' + self.action.target_type)
|
converter = getattr(self, '_convert_target_' + self.action.target_type)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -318,46 +443,40 @@ class AuditLogEntry(Hashable):
|
|||||||
return converter(self._target_id)
|
return converter(self._target_id)
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def category(self):
|
def category(self) -> enums.AuditLogActionCategory:
|
||||||
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
||||||
return self.action.category
|
return self.action.category
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def changes(self):
|
def changes(self) -> AuditLogChanges:
|
||||||
""":class:`AuditLogChanges`: The list of changes this entry has."""
|
""":class:`AuditLogChanges`: The list of changes this entry has."""
|
||||||
obj = AuditLogChanges(self, self._changes)
|
obj = AuditLogChanges(self, self._changes)
|
||||||
del self._changes
|
del self._changes
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def before(self):
|
def before(self) -> AuditLogDiff:
|
||||||
""":class:`AuditLogDiff`: The target's prior state."""
|
""":class:`AuditLogDiff`: The target's prior state."""
|
||||||
return self.changes.before
|
return self.changes.before
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def after(self):
|
def after(self) -> AuditLogDiff:
|
||||||
""":class:`AuditLogDiff`: The target's subsequent state."""
|
""":class:`AuditLogDiff`: The target's subsequent state."""
|
||||||
return self.changes.after
|
return self.changes.after
|
||||||
|
|
||||||
def _convert_target_guild(self, target_id):
|
def _convert_target_guild(self, target_id: int) -> Guild:
|
||||||
return self.guild
|
return self.guild
|
||||||
|
|
||||||
def _convert_target_channel(self, target_id):
|
def _convert_target_channel(self, target_id: int) -> Union[abc.GuildChannel, Object]:
|
||||||
ch = self.guild.get_channel(target_id)
|
return self.guild.get_channel(target_id) or Object(id=target_id)
|
||||||
if ch is None:
|
|
||||||
return Object(id=target_id)
|
|
||||||
return ch
|
|
||||||
|
|
||||||
def _convert_target_user(self, target_id):
|
def _convert_target_user(self, target_id: int) -> Union[Member, User, None]:
|
||||||
return self._get_member(target_id)
|
return self._get_member(target_id)
|
||||||
|
|
||||||
def _convert_target_role(self, target_id):
|
def _convert_target_role(self, target_id: int) -> Union[Role, Object]:
|
||||||
role = self.guild.get_role(target_id)
|
return self.guild.get_role(target_id) or Object(id=target_id)
|
||||||
if role is None:
|
|
||||||
return Object(id=target_id)
|
|
||||||
return role
|
|
||||||
|
|
||||||
def _convert_target_invite(self, target_id):
|
def _convert_target_invite(self, target_id: int) -> Invite:
|
||||||
# invites have target_id set to null
|
# invites have target_id set to null
|
||||||
# so figure out which change has the full invite data
|
# so figure out which change has the full invite data
|
||||||
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
|
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
|
||||||
@@ -367,20 +486,18 @@ class AuditLogEntry(Hashable):
|
|||||||
'max_uses': changeset.max_uses,
|
'max_uses': changeset.max_uses,
|
||||||
'code': changeset.code,
|
'code': changeset.code,
|
||||||
'temporary': changeset.temporary,
|
'temporary': changeset.temporary,
|
||||||
'channel': changeset.channel,
|
|
||||||
'uses': changeset.uses,
|
'uses': changeset.uses,
|
||||||
'guild': self.guild,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj = Invite(state=self._state, data=fake_payload)
|
obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore
|
||||||
try:
|
try:
|
||||||
obj.inviter = changeset.inviter
|
obj.inviter = changeset.inviter
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _convert_target_emoji(self, target_id):
|
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
|
||||||
return self._state.get_emoji(target_id) or Object(id=target_id)
|
return self._state.get_emoji(target_id) or Object(id=target_id)
|
||||||
|
|
||||||
def _convert_target_message(self, target_id):
|
def _convert_target_message(self, target_id: int) -> Union[Member, User, None]:
|
||||||
return self._get_member(target_id)
|
return self._get_member(target_id)
|
||||||
|
|||||||
@@ -22,16 +22,23 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Callable, Dict, List, Optional, TYPE_CHECKING, Union, overload
|
||||||
|
import datetime
|
||||||
|
|
||||||
import discord.abc
|
import discord.abc
|
||||||
from .permissions import Permissions
|
from .permissions import PermissionOverwrite, Permissions
|
||||||
from .enums import ChannelType, try_enum, VoiceRegion
|
from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
from . import utils
|
from . import utils
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .errors import ClientException, NoMoreItems, InvalidArgument
|
from .errors import ClientException, NoMoreItems, InvalidArgument
|
||||||
|
from .stage_instance import StageInstance
|
||||||
|
from .threads import Thread
|
||||||
|
from .iterators import ArchivedThreadIterator
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'TextChannel',
|
'TextChannel',
|
||||||
@@ -41,9 +48,16 @@ __all__ = (
|
|||||||
'CategoryChannel',
|
'CategoryChannel',
|
||||||
'StoreChannel',
|
'StoreChannel',
|
||||||
'GroupChannel',
|
'GroupChannel',
|
||||||
'_channel_factory',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.threads import ThreadArchiveDuration
|
||||||
|
from .role import Role
|
||||||
|
from .member import Member, VoiceState
|
||||||
|
from .abc import Snowflake, SnowflakeTime
|
||||||
|
from .message import Message
|
||||||
|
from .webhook import Webhook
|
||||||
|
|
||||||
async def _single_delete_strategy(messages):
|
async def _single_delete_strategy(messages):
|
||||||
for m in messages:
|
for m in messages:
|
||||||
await m.delete()
|
await m.delete()
|
||||||
@@ -92,6 +106,12 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
in this channel. A value of `0` denotes that it is disabled.
|
in this channel. A value of `0` denotes that it is disabled.
|
||||||
Bots and users with :attr:`~Permissions.manage_channels` or
|
Bots and users with :attr:`~Permissions.manage_channels` or
|
||||||
:attr:`~Permissions.manage_messages` bypass slowmode.
|
:attr:`~Permissions.manage_messages` bypass slowmode.
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
If the channel is marked as "not safe for work".
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('name', 'id', 'guild', 'topic', '_state', 'nsfw',
|
__slots__ = ('name', 'id', 'guild', 'topic', '_state', 'nsfw',
|
||||||
@@ -155,6 +175,14 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
||||||
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def threads(self):
|
||||||
|
"""List[:class:`Thread`]: Returns all the threads that you can see.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return [thread for thread in self.guild.threads if thread.parent_id == self.id]
|
||||||
|
|
||||||
def is_nsfw(self):
|
def is_nsfw(self):
|
||||||
""":class:`bool`: Checks if the channel is NSFW."""
|
""":class:`bool`: Checks if the channel is NSFW."""
|
||||||
return self.nsfw
|
return self.nsfw
|
||||||
@@ -184,6 +212,27 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
"""
|
"""
|
||||||
return self._state._get_message(self.last_message_id) if self.last_message_id else None
|
return self._state._get_message(self.last_message_id) if self.last_message_id else None
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
name: str = ...,
|
||||||
|
topic: str = ...,
|
||||||
|
position: int = ...,
|
||||||
|
nsfw: bool = ...,
|
||||||
|
sync_permissions: bool = ...,
|
||||||
|
category: Optional[CategoryChannel] = ...,
|
||||||
|
slowmode_delay: int = ...,
|
||||||
|
type: ChannelType = ...,
|
||||||
|
overwrites: Dict[Union[Role, Member, Snowflake], PermissionOverwrite] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **options):
|
async def edit(self, *, reason=None, **options):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -240,7 +289,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
await self._edit(options, reason=reason)
|
await self._edit(options, reason=reason)
|
||||||
|
|
||||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self, *, name: str = None, reason: str = None) -> TextChannel:
|
||||||
return await self._clone_impl({
|
return await self._clone_impl({
|
||||||
'topic': self.topic,
|
'topic': self.topic,
|
||||||
'nsfw': self.nsfw,
|
'nsfw': self.nsfw,
|
||||||
@@ -263,8 +312,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
You must have the :attr:`~Permissions.manage_messages` permission to
|
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||||
use this.
|
use this.
|
||||||
|
|
||||||
Usable only by bot accounts.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
messages: Iterable[:class:`abc.Snowflake`]
|
messages: Iterable[:class:`abc.Snowflake`]
|
||||||
@@ -275,8 +322,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
ClientException
|
ClientException
|
||||||
The number of messages to delete was more than 100.
|
The number of messages to delete was more than 100.
|
||||||
Forbidden
|
Forbidden
|
||||||
You do not have proper permissions to delete the messages or
|
You do not have proper permissions to delete the messages.
|
||||||
you're not using a bot account.
|
|
||||||
NotFound
|
NotFound
|
||||||
If single delete, then the message was already deleted.
|
If single delete, then the message was already deleted.
|
||||||
HTTPException
|
HTTPException
|
||||||
@@ -299,7 +345,17 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
message_ids = [m.id for m in messages]
|
message_ids = [m.id for m in messages]
|
||||||
await self._state.http.delete_messages(self.id, message_ids)
|
await self._state.http.delete_messages(self.id, message_ids)
|
||||||
|
|
||||||
async def purge(self, *, limit=100, check=None, before=None, after=None, around=None, oldest_first=False, bulk=True):
|
async def purge(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
limit: int = 100,
|
||||||
|
check: Callable[[Message], bool] = None,
|
||||||
|
before: Optional[SnowflakeTime] = None,
|
||||||
|
after: Optional[SnowflakeTime] = None,
|
||||||
|
around: Optional[SnowflakeTime] = None,
|
||||||
|
oldest_first: Optional[bool] = False,
|
||||||
|
bulk: bool = True,
|
||||||
|
) -> List[Message]:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Purges a list of messages that meet the criteria given by the predicate
|
Purges a list of messages that meet the criteria given by the predicate
|
||||||
@@ -307,8 +363,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
without discrimination.
|
without discrimination.
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_messages` permission to
|
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||||
delete messages even if they are your own (unless you are a user
|
delete messages even if they are your own.
|
||||||
account). The :attr:`~Permissions.read_message_history` permission is
|
The :attr:`~Permissions.read_message_history` permission is
|
||||||
also needed to retrieve message history.
|
also needed to retrieve message history.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
@@ -320,7 +376,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
return m.author == client.user
|
return m.author == client.user
|
||||||
|
|
||||||
deleted = await channel.purge(limit=100, check=is_me)
|
deleted = await channel.purge(limit=100, check=is_me)
|
||||||
await channel.send('Deleted {} message(s)'.format(len(deleted)))
|
await channel.send(f'Deleted {len(deleted)} message(s)')
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
@@ -425,7 +481,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
data = await self._state.http.channel_webhooks(self.id)
|
data = await self._state.http.channel_webhooks(self.id)
|
||||||
return [Webhook.from_state(d, state=self._state) for d in data]
|
return [Webhook.from_state(d, state=self._state) for d in data]
|
||||||
|
|
||||||
async def create_webhook(self, *, name, avatar=None, reason=None):
|
async def create_webhook(self, *, name: str, avatar: bytes = None, reason: str = None) -> Webhook:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Creates a webhook for this channel.
|
Creates a webhook for this channel.
|
||||||
@@ -465,7 +521,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason)
|
data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason)
|
||||||
return Webhook.from_state(data, state=self._state)
|
return Webhook.from_state(data, state=self._state)
|
||||||
|
|
||||||
async def follow(self, *, destination, reason=None):
|
async def follow(self, *, destination: TextChannel, reason: Optional[str] = None) -> Webhook:
|
||||||
"""
|
"""
|
||||||
Follows a channel using a webhook.
|
Follows a channel using a webhook.
|
||||||
|
|
||||||
@@ -504,7 +560,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
raise ClientException('The channel must be a news channel.')
|
raise ClientException('The channel must be a news channel.')
|
||||||
|
|
||||||
if not isinstance(destination, TextChannel):
|
if not isinstance(destination, TextChannel):
|
||||||
raise InvalidArgument('Expected TextChannel received {0.__name__}'.format(type(destination)))
|
raise InvalidArgument(f'Expected TextChannel received {destination.__class__.__name__}')
|
||||||
|
|
||||||
from .webhook import Webhook
|
from .webhook import Webhook
|
||||||
data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason)
|
data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason)
|
||||||
@@ -532,10 +588,146 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
from .message import PartialMessage
|
from .message import PartialMessage
|
||||||
return PartialMessage(channel=self, id=message_id)
|
return PartialMessage(channel=self, id=message_id)
|
||||||
|
|
||||||
|
def get_thread(self, thread_id: int) -> Optional[Thread]:
|
||||||
|
"""Returns a thread with the given ID.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
thread_id: :class:`int`
|
||||||
|
The ID to search for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Optional[:class:`Thread`]
|
||||||
|
The returned thread or ``None`` if not found.
|
||||||
|
"""
|
||||||
|
return self.guild.get_thread(thread_id)
|
||||||
|
|
||||||
|
async def start_thread(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
message: Optional[Snowflake] = None,
|
||||||
|
auto_archive_duration: ThreadArchiveDuration = 1440,
|
||||||
|
) -> Thread:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Starts a thread in this text channel.
|
||||||
|
|
||||||
|
If no starter message is passed with the ``message`` parameter then
|
||||||
|
you must have :attr:`~discord.Permissions.send_messages` and
|
||||||
|
:attr:`~discord.Permissions.use_private_threads` in order to start the thread.
|
||||||
|
|
||||||
|
If a starter message is passed with the ``message`` parameter then
|
||||||
|
you must have :attr:`~discord.Permissions.send_messages` and
|
||||||
|
:attr:`~discord.Permissions.use_threads` in order to start the thread.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the thread.
|
||||||
|
message: Optional[:class:`abc.Snowflake`]
|
||||||
|
A snowflake representing the message to start the thread with.
|
||||||
|
If ``None`` is passed then a private thread is started.
|
||||||
|
Defaults to ``None``.
|
||||||
|
auto_archive_duration: :class:`int`
|
||||||
|
The duration in minutes before a thread is automatically archived for inactivity.
|
||||||
|
Defaults to ``1440`` or 24 hours.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to start a thread.
|
||||||
|
HTTPException
|
||||||
|
Starting the thread failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if message is None:
|
||||||
|
data = await self._state.http.start_private_thread(
|
||||||
|
self.id,
|
||||||
|
name=name,
|
||||||
|
auto_archive_duration=auto_archive_duration,
|
||||||
|
type=ChannelType.private_thread.value,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data = await self._state.http.start_public_thread(
|
||||||
|
self.id,
|
||||||
|
message.id,
|
||||||
|
name=name,
|
||||||
|
auto_archive_duration=auto_archive_duration,
|
||||||
|
type=ChannelType.public_thread.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Thread(guild=self.guild, data=data)
|
||||||
|
|
||||||
|
def archived_threads(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
private: bool = False,
|
||||||
|
joined: bool = False,
|
||||||
|
limit: Optional[int] = 50,
|
||||||
|
before: Optional[Union[Snowflake, datetime.datetime]] = None,
|
||||||
|
) -> ArchivedThreadIterator:
|
||||||
|
"""Returns an :class:`~discord.AsyncIterator` that iterates over all archived threads in the guild.
|
||||||
|
|
||||||
|
You must have :attr:`~Permissions.read_message_history` to use this. If iterating over private threads
|
||||||
|
then :attr:`~Permissions.manage_threads` is also required.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
limit: Optional[:class:`bool`]
|
||||||
|
The number of threads to retrieve.
|
||||||
|
If ``None``, retrieves every archived thread in the channel. Note, however,
|
||||||
|
that this would make it a slow operation.
|
||||||
|
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||||
|
Retrieve archived channels before the given date or ID.
|
||||||
|
private: :class:`bool`
|
||||||
|
Whether to retrieve private archived threads.
|
||||||
|
joined: :class:`bool`
|
||||||
|
Whether to retrieve private archived threads that you've joined.
|
||||||
|
You cannot set ``joined`` to ``True`` and ``private`` to ``False``.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to get archived threads.
|
||||||
|
HTTPException
|
||||||
|
The request to get the archived threads failed.
|
||||||
|
|
||||||
|
Yields
|
||||||
|
-------
|
||||||
|
:class:`Thread`
|
||||||
|
The archived threads.
|
||||||
|
"""
|
||||||
|
return ArchivedThreadIterator(self.id, self.guild, limit=limit, joined=joined, private=private, before=before)
|
||||||
|
|
||||||
|
async def active_threads(self) -> List[Thread]:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Returns a list of active :class:`Thread` that the client can access.
|
||||||
|
|
||||||
|
This includes both private and public threads.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
The request to get the active threads failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
List[:class:`Thread`]
|
||||||
|
The archived threads
|
||||||
|
"""
|
||||||
|
data = await self._state.http.get_active_threads(self.id)
|
||||||
|
# TODO: thread members?
|
||||||
|
return [Thread(guild=self.guild, data=d) for d in data.get('threads', [])]
|
||||||
|
|
||||||
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||||
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
|
||||||
'_state', 'position', '_overwrites', 'category_id',
|
'_state', 'position', '_overwrites', 'category_id',
|
||||||
'rtc_region')
|
'rtc_region', 'video_quality_mode')
|
||||||
|
|
||||||
def __init__(self, *, state, guild, data):
|
def __init__(self, *, state, guild, data):
|
||||||
self._state = state
|
self._state = state
|
||||||
@@ -554,6 +746,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
|
|||||||
self.rtc_region = data.get('rtc_region')
|
self.rtc_region = data.get('rtc_region')
|
||||||
if self.rtc_region:
|
if self.rtc_region:
|
||||||
self.rtc_region = try_enum(VoiceRegion, self.rtc_region)
|
self.rtc_region = try_enum(VoiceRegion, self.rtc_region)
|
||||||
|
self.video_quality_mode = try_enum(VideoQualityMode, data.get('video_quality_mode', 1))
|
||||||
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
self.category_id = utils._get_as_snowflake(data, 'parent_id')
|
||||||
self.position = data['position']
|
self.position = data['position']
|
||||||
self.bitrate = data.get('bitrate')
|
self.bitrate = data.get('bitrate')
|
||||||
@@ -565,7 +758,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
|
|||||||
return ChannelType.voice.value
|
return ChannelType.voice.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def members(self):
|
def members(self) -> List[Member]:
|
||||||
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
|
||||||
ret = []
|
ret = []
|
||||||
for user_id, state in self.guild._voice_states.items():
|
for user_id, state in self.guild._voice_states.items():
|
||||||
@@ -576,7 +769,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def voice_states(self):
|
def voice_states(self) -> Dict[int, VoiceState]:
|
||||||
"""Returns a mapping of member IDs who have voice states in this channel.
|
"""Returns a mapping of member IDs who have voice states in this channel.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
@@ -594,7 +787,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
|
|||||||
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
|
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)
|
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
|
||||||
def permissions_for(self, member):
|
def permissions_for(self, member: Union[Role, Member], /) -> Permissions:
|
||||||
base = super().permissions_for(member)
|
base = super().permissions_for(member)
|
||||||
|
|
||||||
# voice channels cannot be edited by people who can't connect to them
|
# voice channels cannot be edited by people who can't connect to them
|
||||||
@@ -648,6 +841,10 @@ class VoiceChannel(VocalGuildChannel):
|
|||||||
A value of ``None`` indicates automatic voice region detection.
|
A value of ``None`` indicates automatic voice region detection.
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
|
video_quality_mode: :class:`VideoQualityMode`
|
||||||
|
The camera video quality for the voice channel's participants.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
@@ -659,6 +856,7 @@ class VoiceChannel(VocalGuildChannel):
|
|||||||
('rtc_region', self.rtc_region),
|
('rtc_region', self.rtc_region),
|
||||||
('position', self.position),
|
('position', self.position),
|
||||||
('bitrate', self.bitrate),
|
('bitrate', self.bitrate),
|
||||||
|
('video_quality_mode', self.video_quality_mode),
|
||||||
('user_limit', self.user_limit),
|
('user_limit', self.user_limit),
|
||||||
('category_id', self.category_id)
|
('category_id', self.category_id)
|
||||||
]
|
]
|
||||||
@@ -671,12 +869,33 @@ class VoiceChannel(VocalGuildChannel):
|
|||||||
return ChannelType.voice
|
return ChannelType.voice
|
||||||
|
|
||||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self, *, name: str = None, reason: str = None) -> VoiceChannel:
|
||||||
return await self._clone_impl({
|
return await self._clone_impl({
|
||||||
'bitrate': self.bitrate,
|
'bitrate': self.bitrate,
|
||||||
'user_limit': self.user_limit
|
'user_limit': self.user_limit
|
||||||
}, name=name, reason=reason)
|
}, name=name, reason=reason)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
name: str = ...,
|
||||||
|
bitrate: int = ...,
|
||||||
|
user_limit: int = ...,
|
||||||
|
position: int = ...,
|
||||||
|
sync_permissions: int = ...,
|
||||||
|
category: Optional[CategoryChannel] = ...,
|
||||||
|
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
|
||||||
|
rtc_region: Optional[VoiceRegion] = ...,
|
||||||
|
video_quality_mode: VideoQualityMode = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **options):
|
async def edit(self, *, reason=None, **options):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -714,6 +933,10 @@ class VoiceChannel(VocalGuildChannel):
|
|||||||
A value of ``None`` indicates automatic voice region detection.
|
A value of ``None`` indicates automatic voice region detection.
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
|
video_quality_mode: :class:`VideoQualityMode`
|
||||||
|
The camera video quality for the voice channel's participants.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@@ -772,6 +995,10 @@ class StageChannel(VocalGuildChannel):
|
|||||||
rtc_region: Optional[:class:`VoiceRegion`]
|
rtc_region: Optional[:class:`VoiceRegion`]
|
||||||
The region for the stage channel's voice communication.
|
The region for the stage channel's voice communication.
|
||||||
A value of ``None`` indicates automatic voice region detection.
|
A value of ``None`` indicates automatic voice region detection.
|
||||||
|
video_quality_mode: :class:`VideoQualityMode`
|
||||||
|
The camera video quality for the stage channel's participants.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
__slots__ = ('topic',)
|
__slots__ = ('topic',)
|
||||||
|
|
||||||
@@ -783,6 +1010,7 @@ class StageChannel(VocalGuildChannel):
|
|||||||
('rtc_region', self.rtc_region),
|
('rtc_region', self.rtc_region),
|
||||||
('position', self.position),
|
('position', self.position),
|
||||||
('bitrate', self.bitrate),
|
('bitrate', self.bitrate),
|
||||||
|
('video_quality_mode', self.video_quality_mode),
|
||||||
('user_limit', self.user_limit),
|
('user_limit', self.user_limit),
|
||||||
('category_id', self.category_id)
|
('category_id', self.category_id)
|
||||||
]
|
]
|
||||||
@@ -794,20 +1022,139 @@ class StageChannel(VocalGuildChannel):
|
|||||||
self.topic = data.get('topic')
|
self.topic = data.get('topic')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requesting_to_speak(self):
|
def requesting_to_speak(self) -> List[Member]:
|
||||||
"""List[:class:`Member`]: A list of members who are requesting to speak in the stage channel."""
|
"""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]
|
return [member for member in self.members if member.voice.requested_to_speak_at is not None]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speakers(self) -> List[Member]:
|
||||||
|
"""List[:class:`Member`]: A list of members who have been permitted to speak in the stage channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return [member for member in self.members if not member.voice.suppress and member.voice.requested_to_speak_at is None]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def listeners(self) -> List[Member]:
|
||||||
|
"""List[:class:`Member`]: A list of members who are listening in the stage channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return [member for member in self.members if member.voice.suppress]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def moderators(self) -> List[Member]:
|
||||||
|
"""List[:class:`Member`]: A list of members who are moderating the stage channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
required_permissions = Permissions.stage_moderator()
|
||||||
|
return [member for member in self.members if self.permissions_for(member) >= required_permissions]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
""":class:`ChannelType`: The channel's Discord type."""
|
""":class:`ChannelType`: The channel's Discord type."""
|
||||||
return ChannelType.stage_voice
|
return ChannelType.stage_voice
|
||||||
|
|
||||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> StageChannel:
|
||||||
return await self._clone_impl({
|
return await self._clone_impl({}, name=name, reason=reason)
|
||||||
'topic': self.topic,
|
|
||||||
}, name=name, reason=reason)
|
@property
|
||||||
|
def instance(self) -> Optional[StageInstance]:
|
||||||
|
"""Optional[:class:`StageInstance`]: The running stage instance of the stage channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return utils.get(self.guild.stage_instances, channel_id=self.id)
|
||||||
|
|
||||||
|
async def create_instance(self, *, topic: str, privacy_level: StagePrivacyLevel = utils.MISSING) -> StageInstance:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Create a stage instance.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||||
|
use this.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
topic: :class:`str`
|
||||||
|
The stage instance's topic.
|
||||||
|
privacy_level: :class:`StagePrivacyLevel`
|
||||||
|
The stage instance's privacy level. Defaults to :attr:`PrivacyLevel.guild_only`.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
InvalidArgument
|
||||||
|
If the ``privacy_level`` parameter is not the proper type.
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to create a stage instance.
|
||||||
|
HTTPException
|
||||||
|
Creating a stage instance failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`StageInstance`
|
||||||
|
The newly created stage instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'channel_id': self.id,
|
||||||
|
'topic': topic
|
||||||
|
}
|
||||||
|
|
||||||
|
if privacy_level is not utils.MISSING:
|
||||||
|
if not isinstance(privacy_level, StagePrivacyLevel):
|
||||||
|
raise InvalidArgument('privacy_level field must be of type PrivacyLevel')
|
||||||
|
|
||||||
|
payload['privacy_level'] = privacy_level.value
|
||||||
|
|
||||||
|
data = await self._state.http.create_stage_instance(**payload)
|
||||||
|
return StageInstance(guild=self.guild, state=self._state, data=data)
|
||||||
|
|
||||||
|
async def fetch_instance(self) -> StageInstance:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Gets the running :class:`StageInstance`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:exc:`.NotFound`
|
||||||
|
The stage instance or channel could not be found.
|
||||||
|
:exc:`.HTTPException`
|
||||||
|
Getting the stage instance failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`StageInstance`
|
||||||
|
The stage instance.
|
||||||
|
"""
|
||||||
|
data = await self._state.http.get_stage_instance(self.id)
|
||||||
|
return StageInstance(guild=self.guild, state=self._state, data=data)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
name: str = ...,
|
||||||
|
topic: Optional[str] = ...,
|
||||||
|
position: int = ...,
|
||||||
|
sync_permissions: int = ...,
|
||||||
|
category: Optional[CategoryChannel] = ...,
|
||||||
|
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
|
||||||
|
rtc_region: Optional[VoiceRegion] = ...,
|
||||||
|
video_quality_mode: VideoQualityMode = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **options):
|
async def edit(self, *, reason=None, **options):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@@ -817,12 +1164,13 @@ class StageChannel(VocalGuildChannel):
|
|||||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||||
use this.
|
use this.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
The ``topic`` parameter must now be set via :attr:`create_instance`.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The new channel's name.
|
The new channel's name.
|
||||||
topic: :class:`str`
|
|
||||||
The new channel's topic.
|
|
||||||
position: :class:`int`
|
position: :class:`int`
|
||||||
The new channel's position.
|
The new channel's position.
|
||||||
sync_permissions: :class:`bool`
|
sync_permissions: :class:`bool`
|
||||||
@@ -839,6 +1187,10 @@ class StageChannel(VocalGuildChannel):
|
|||||||
rtc_region: Optional[:class:`VoiceRegion`]
|
rtc_region: Optional[:class:`VoiceRegion`]
|
||||||
The new region for the stage channel's voice communication.
|
The new region for the stage channel's voice communication.
|
||||||
A value of ``None`` indicates automatic voice region detection.
|
A value of ``None`` indicates automatic voice region detection.
|
||||||
|
video_quality_mode: :class:`VideoQualityMode`
|
||||||
|
The camera video quality for the stage channel's participants.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@@ -851,7 +1203,6 @@ class StageChannel(VocalGuildChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
await self._edit(options, reason=reason)
|
await self._edit(options, reason=reason)
|
||||||
|
|
||||||
class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||||
"""Represents a Discord channel category.
|
"""Represents a Discord channel category.
|
||||||
|
|
||||||
@@ -886,6 +1237,12 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
position: :class:`int`
|
position: :class:`int`
|
||||||
The position in the category list. This is a number that starts at 0. e.g. the
|
The position in the category list. This is a number that starts at 0. e.g. the
|
||||||
top category is position 0.
|
top category is position 0.
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
If the channel is marked as "not safe for work".
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('name', 'id', 'guild', 'nsfw', '_state', 'position', '_overwrites', 'category_id')
|
__slots__ = ('name', 'id', 'guild', 'nsfw', '_state', 'position', '_overwrites', 'category_id')
|
||||||
@@ -896,7 +1253,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
self._update(guild, data)
|
self._update(guild, data)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<CategoryChannel id={0.id} name={0.name!r} position={0.position} nsfw={0.nsfw}>'.format(self)
|
return f'<CategoryChannel id={self.id} name={self.name!r} position={self.position} nsfw={self.nsfw}>'
|
||||||
|
|
||||||
def _update(self, guild, data):
|
def _update(self, guild, data):
|
||||||
self.guild = guild
|
self.guild = guild
|
||||||
@@ -920,11 +1277,27 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
return self.nsfw
|
return self.nsfw
|
||||||
|
|
||||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> CategoryChannel:
|
||||||
return await self._clone_impl({
|
return await self._clone_impl({
|
||||||
'nsfw': self.nsfw
|
'nsfw': self.nsfw
|
||||||
}, name=name, reason=reason)
|
}, name=name, reason=reason)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
name: str = ...,
|
||||||
|
position: int = ...,
|
||||||
|
nsfw: bool = ...,
|
||||||
|
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **options):
|
async def edit(self, *, reason=None, **options):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -1000,7 +1373,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def stage_channels(self):
|
def stage_channels(self):
|
||||||
"""List[:class:`StageChannel`]: Returns the voice channels that are under this category.
|
"""List[:class:`StageChannel`]: Returns the stage channels that are under this category.
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
"""
|
"""
|
||||||
@@ -1082,6 +1455,12 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
position: :class:`int`
|
position: :class:`int`
|
||||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||||
top channel is position 0.
|
top channel is position 0.
|
||||||
|
nsfw: :class:`bool`
|
||||||
|
If the channel is marked as "not safe for work".
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('name', 'id', 'guild', '_state', 'nsfw',
|
__slots__ = ('name', 'id', 'guild', '_state', 'nsfw',
|
||||||
'category_id', 'position', '_overwrites',)
|
'category_id', 'position', '_overwrites',)
|
||||||
@@ -1092,7 +1471,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
self._update(guild, data)
|
self._update(guild, data)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<StoreChannel id={0.id} name={0.name!r} position={0.position} nsfw={0.nsfw}>'.format(self)
|
return f'<StoreChannel id={self.id} name={self.name!r} position={self.position} nsfw={self.nsfw}>'
|
||||||
|
|
||||||
def _update(self, guild, data):
|
def _update(self, guild, data):
|
||||||
self.guild = guild
|
self.guild = guild
|
||||||
@@ -1125,11 +1504,29 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
return self.nsfw
|
return self.nsfw
|
||||||
|
|
||||||
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
@utils.copy_doc(discord.abc.GuildChannel.clone)
|
||||||
async def clone(self, *, name=None, reason=None):
|
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> StoreChannel:
|
||||||
return await self._clone_impl({
|
return await self._clone_impl({
|
||||||
'nsfw': self.nsfw
|
'nsfw': self.nsfw
|
||||||
}, name=name, reason=reason)
|
}, name=name, reason=reason)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str = ...,
|
||||||
|
position: int = ...,
|
||||||
|
nsfw: bool = ...,
|
||||||
|
sync_permissions: bool = ...,
|
||||||
|
category: Optional[CategoryChannel],
|
||||||
|
reason: Optional[str],
|
||||||
|
overwrites: Dict[Union[Role, Member], PermissionOverwrite]
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **options):
|
async def edit(self, *, reason=None, **options):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -1195,8 +1592,10 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
recipient: :class:`User`
|
recipient: Optional[:class:`User`]
|
||||||
The user you are participating with in the direct message channel.
|
The user you are participating with in the direct message channel.
|
||||||
|
If this channel is received through the gateway, the recipient information
|
||||||
|
may not be always available.
|
||||||
me: :class:`ClientUser`
|
me: :class:`ClientUser`
|
||||||
The user presenting yourself.
|
The user presenting yourself.
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
@@ -1215,10 +1614,21 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Direct Message with {self.recipient}'
|
if self.recipient:
|
||||||
|
return f'Direct Message with {self.recipient}'
|
||||||
|
return 'Direct Message with Unknown User'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self)
|
return f'<DMChannel id={self.id} recipient={self.recipient!r}>'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_message(cls, state, channel_id):
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self._state = state
|
||||||
|
self.id = channel_id
|
||||||
|
self.recipient = None
|
||||||
|
self.me = state.user
|
||||||
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
@@ -1255,6 +1665,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base = Permissions.text()
|
base = Permissions.text()
|
||||||
|
base.read_messages = True
|
||||||
base.send_tts_messages = False
|
base.send_tts_messages = False
|
||||||
base.manage_messages = False
|
base.manage_messages = False
|
||||||
return base
|
return base
|
||||||
@@ -1312,13 +1723,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
The group channel ID.
|
The group channel ID.
|
||||||
owner: :class:`User`
|
owner: :class:`User`
|
||||||
The user that owns the group channel.
|
The user that owns the group channel.
|
||||||
icon: Optional[:class:`str`]
|
|
||||||
The group channel's icon hash if provided.
|
|
||||||
name: Optional[:class:`str`]
|
name: Optional[:class:`str`]
|
||||||
The group channel's name if provided.
|
The group channel's name if provided.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state')
|
__slots__ = ('id', 'recipients', 'owner', '_icon', 'name', 'me', '_state')
|
||||||
|
|
||||||
def __init__(self, *, me, state, data):
|
def __init__(self, *, me, state, data):
|
||||||
self._state = state
|
self._state = state
|
||||||
@@ -1328,7 +1737,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
def _update_group(self, data):
|
def _update_group(self, data):
|
||||||
owner_id = utils._get_as_snowflake(data, 'owner_id')
|
owner_id = utils._get_as_snowflake(data, 'owner_id')
|
||||||
self.icon = data.get('icon')
|
self._icon = data.get('icon')
|
||||||
self.name = data.get('name')
|
self.name = data.get('name')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1354,7 +1763,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
return ', '.join(map(lambda x: x.name, self.recipients))
|
return ', '.join(map(lambda x: x.name, self.recipients))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<GroupChannel id={0.id} name={0.name!r}>'.format(self)
|
return f'<GroupChannel id={self.id} name={self.name!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
@@ -1362,40 +1771,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
return ChannelType.group
|
return ChannelType.group
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon_url(self):
|
def icon(self):
|
||||||
""":class:`Asset`: Returns the channel's icon asset if available.
|
"""Optional[:class:`Asset`]: Returns the channel's icon asset if available."""
|
||||||
|
if self._icon is None:
|
||||||
This is equivalent to calling :meth:`icon_url_as` with
|
return None
|
||||||
the default parameters ('webp' format and a size of 1024).
|
return Asset._from_icon(self._state, self.id, self._icon, path='channel')
|
||||||
"""
|
|
||||||
return self.icon_url_as()
|
|
||||||
|
|
||||||
def icon_url_as(self, *, format='webp', size=1024):
|
|
||||||
"""Returns an :class:`Asset` for the icon the channel has.
|
|
||||||
|
|
||||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
|
||||||
The size must be a power of 2 between 16 and 4096.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
format: :class:`str`
|
|
||||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_icon(self._state, self, 'channel', format=format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self):
|
||||||
@@ -1428,6 +1808,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base = Permissions.text()
|
base = Permissions.text()
|
||||||
|
base.read_messages = True
|
||||||
base.send_tts_messages = False
|
base.send_tts_messages = False
|
||||||
base.manage_messages = False
|
base.manage_messages = False
|
||||||
base.mention_everyone = True
|
base.mention_everyone = True
|
||||||
@@ -1452,18 +1833,19 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
await self._state.http.leave_group(self.id)
|
await self._state.http.leave_group(self.id)
|
||||||
|
|
||||||
def _channel_factory(channel_type):
|
def _coerce_channel_type(value: Union[ChannelType, int]) -> ChannelType:
|
||||||
value = try_enum(ChannelType, channel_type)
|
if isinstance(value, ChannelType):
|
||||||
|
return value
|
||||||
|
return try_enum(ChannelType, value)
|
||||||
|
|
||||||
|
def _guild_channel_factory(channel_type: Union[ChannelType, int]):
|
||||||
|
value = _coerce_channel_type(channel_type)
|
||||||
if value is ChannelType.text:
|
if value is ChannelType.text:
|
||||||
return TextChannel, value
|
return TextChannel, value
|
||||||
elif value is ChannelType.voice:
|
elif value is ChannelType.voice:
|
||||||
return VoiceChannel, value
|
return VoiceChannel, value
|
||||||
elif value is ChannelType.private:
|
|
||||||
return DMChannel, value
|
|
||||||
elif value is ChannelType.category:
|
elif value is ChannelType.category:
|
||||||
return CategoryChannel, value
|
return CategoryChannel, value
|
||||||
elif value is ChannelType.group:
|
|
||||||
return GroupChannel, value
|
|
||||||
elif value is ChannelType.news:
|
elif value is ChannelType.news:
|
||||||
return TextChannel, value
|
return TextChannel, value
|
||||||
elif value is ChannelType.store:
|
elif value is ChannelType.store:
|
||||||
@@ -1472,3 +1854,12 @@ def _channel_factory(channel_type):
|
|||||||
return StageChannel, value
|
return StageChannel, value
|
||||||
else:
|
else:
|
||||||
return None, value
|
return None, value
|
||||||
|
|
||||||
|
def _channel_factory(channel_type: Union[ChannelType, int]):
|
||||||
|
cls, value = _guild_channel_factory(channel_type)
|
||||||
|
if value is ChannelType.private:
|
||||||
|
return DMChannel, value
|
||||||
|
elif value is ChannelType.group:
|
||||||
|
return GroupChannel, value
|
||||||
|
else:
|
||||||
|
return cls, value
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Any, Generator, List, Optional, Sequence, TYPE_CHECKING, TypeVar, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ from .enums import ChannelType
|
|||||||
from .mentions import AllowedMentions
|
from .mentions import AllowedMentions
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .enums import Status, VoiceRegion
|
from .enums import Status, VoiceRegion
|
||||||
|
from .flags import ApplicationFlags
|
||||||
from .gateway import *
|
from .gateway import *
|
||||||
from .activity import BaseActivity, create_activity
|
from .activity import BaseActivity, create_activity
|
||||||
from .voice_client import VoiceClient
|
from .voice_client import VoiceClient
|
||||||
@@ -51,11 +55,16 @@ from .backoff import ExponentialBackoff
|
|||||||
from .webhook import Webhook
|
from .webhook import Webhook
|
||||||
from .iterators import GuildIterator
|
from .iterators import GuildIterator
|
||||||
from .appinfo import AppInfo
|
from .appinfo import AppInfo
|
||||||
|
from .ui.view import View
|
||||||
|
from .stage_instance import StageInstance
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Client',
|
'Client',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .abc import SnowflakeTime
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _cancel_tasks(loop):
|
def _cancel_tasks(loop):
|
||||||
@@ -131,8 +140,6 @@ class Client:
|
|||||||
currently selected intents.
|
currently selected intents.
|
||||||
|
|
||||||
.. versionadded:: 1.5
|
.. versionadded:: 1.5
|
||||||
fetch_offline_members: :class:`bool`
|
|
||||||
A deprecated alias of ``chunk_guilds_at_startup``.
|
|
||||||
chunk_guilds_at_startup: :class:`bool`
|
chunk_guilds_at_startup: :class:`bool`
|
||||||
Indicates if :func:`.on_ready` should be delayed to chunk all guilds
|
Indicates if :func:`.on_ready` should be delayed to chunk all guilds
|
||||||
at start-up if necessary. This operation is incredibly slow for large
|
at start-up if necessary. This operation is incredibly slow for large
|
||||||
@@ -158,37 +165,6 @@ class Client:
|
|||||||
preparing the member cache and firing READY. The default timeout is 2 seconds.
|
preparing the member cache and firing READY. The default timeout is 2 seconds.
|
||||||
|
|
||||||
.. versionadded:: 1.4
|
.. versionadded:: 1.4
|
||||||
guild_subscriptions: :class:`bool`
|
|
||||||
Whether to dispatch presence or typing events. Defaults to ``True``.
|
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
If this is set to ``False`` then the following features will be disabled:
|
|
||||||
|
|
||||||
- No user related updates (:func:`on_user_update` will not dispatch)
|
|
||||||
- All member related events will be disabled.
|
|
||||||
- :func:`on_member_update`
|
|
||||||
- :func:`on_member_join`
|
|
||||||
- :func:`on_member_remove`
|
|
||||||
|
|
||||||
- Typing events will be disabled (:func:`on_typing`).
|
|
||||||
- If ``fetch_offline_members`` is set to ``False`` then the user cache will not exist.
|
|
||||||
This makes it difficult or impossible to do many things, for example:
|
|
||||||
|
|
||||||
- Computing permissions
|
|
||||||
- Querying members in a voice channel via :attr:`VoiceChannel.members` will be empty.
|
|
||||||
- Most forms of receiving :class:`Member` will be
|
|
||||||
receiving :class:`User` instead, except for message events.
|
|
||||||
- :attr:`Guild.owner` will usually resolve to ``None``.
|
|
||||||
- :meth:`Guild.get_member` will usually be unavailable.
|
|
||||||
- Anything that involves using :class:`Member`.
|
|
||||||
- :attr:`users` will not be as populated.
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
In short, this makes it so the only member you can reliably query is the
|
|
||||||
message author. Useful for bots that do not require any state.
|
|
||||||
assume_unsync_clock: :class:`bool`
|
assume_unsync_clock: :class:`bool`
|
||||||
Whether to assume the system clock is unsynced. This applies to the ratelimit handling
|
Whether to assume the system clock is unsynced. This applies to the ratelimit handling
|
||||||
code. If this is set to ``True``, the default, then the library uses the time to reset
|
code. If this is set to ``True``, the default, then the library uses the time to reset
|
||||||
@@ -203,7 +179,7 @@ class Client:
|
|||||||
ws
|
ws
|
||||||
The websocket gateway the client is currently connected to. Could be ``None``.
|
The websocket gateway the client is currently connected to. Could be ``None``.
|
||||||
loop: :class:`asyncio.AbstractEventLoop`
|
loop: :class:`asyncio.AbstractEventLoop`
|
||||||
The event loop that the client uses for HTTP requests and websocket operations.
|
The event loop that the client uses for asynchronous operations.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *, loop=None, **options):
|
def __init__(self, *, loop=None, **options):
|
||||||
self.ws = None
|
self.ws = None
|
||||||
@@ -318,10 +294,18 @@ class Client:
|
|||||||
|
|
||||||
If this is not passed via ``__init__`` then this is retrieved
|
If this is not passed via ``__init__`` then this is retrieved
|
||||||
through the gateway when an event contains the data. Usually
|
through the gateway when an event contains the data. Usually
|
||||||
after :func:`on_connect` is called.
|
after :func:`~discord.on_connect` is called.
|
||||||
"""
|
"""
|
||||||
return self._connection.application_id
|
return self._connection.application_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def application_flags(self) -> ApplicationFlags:
|
||||||
|
""":class:`~discord.ApplicationFlags`: The client's application flags.
|
||||||
|
|
||||||
|
.. versionadded: 2.0
|
||||||
|
"""
|
||||||
|
return self._connection.application_flags # type: ignore
|
||||||
|
|
||||||
def is_ready(self):
|
def is_ready(self):
|
||||||
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
|
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
|
||||||
return self._ready.is_set()
|
return self._ready.is_set()
|
||||||
@@ -431,14 +415,6 @@ class Client:
|
|||||||
|
|
||||||
Logs in the client with the specified credentials.
|
Logs in the client with the specified credentials.
|
||||||
|
|
||||||
This function can be used in two different ways.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Logging on with a user token is against the Discord
|
|
||||||
`Terms of Service <https://support.discord.com/hc/en-us/articles/115002192352>`_
|
|
||||||
and doing so might potentially get your account banned.
|
|
||||||
Use this at your own risk.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
@@ -551,7 +527,6 @@ class Client:
|
|||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.http.close()
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
for voice in self.voice_clients:
|
for voice in self.voice_clients:
|
||||||
@@ -564,6 +539,7 @@ class Client:
|
|||||||
if self.ws is not None and self.ws.open:
|
if self.ws is not None and self.ws.open:
|
||||||
await self.ws.close(code=1000)
|
await self.ws.close(code=1000)
|
||||||
|
|
||||||
|
await self.http.close()
|
||||||
self._ready.clear()
|
self._ready.clear()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
@@ -690,7 +666,7 @@ class Client:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def intents(self):
|
def intents(self):
|
||||||
""":class:`Intents`: The intents configured for this connection.
|
""":class:`~discord.Intents`: The intents configured for this connection.
|
||||||
|
|
||||||
.. versionadded:: 1.5
|
.. versionadded:: 1.5
|
||||||
"""
|
"""
|
||||||
@@ -718,6 +694,28 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
return self._connection.get_channel(id)
|
return self._connection.get_channel(id)
|
||||||
|
|
||||||
|
def get_stage_instance(self, id) -> Optional[StageInstance]:
|
||||||
|
"""Returns a stage instance with the given stage channel ID.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
id: :class:`int`
|
||||||
|
The ID to search for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Optional[:class:`StageInstance`]
|
||||||
|
The returns stage instance of ``None`` if not found.
|
||||||
|
"""
|
||||||
|
from .channel import StageChannel
|
||||||
|
|
||||||
|
channel = self._connection.get_channel(id)
|
||||||
|
|
||||||
|
if isinstance(channel, StageChannel):
|
||||||
|
return channel.instance
|
||||||
|
|
||||||
def get_guild(self, id):
|
def get_guild(self, id):
|
||||||
"""Returns a guild with the given ID.
|
"""Returns a guild with the given ID.
|
||||||
|
|
||||||
@@ -849,7 +847,7 @@ class Client:
|
|||||||
return m.content == 'hello' and m.channel == channel
|
return m.content == 'hello' and m.channel == channel
|
||||||
|
|
||||||
msg = await client.wait_for('message', check=check)
|
msg = await client.wait_for('message', check=check)
|
||||||
await channel.send('Hello {.author}!'.format(msg))
|
await channel.send(f'Hello {msg.author}!')
|
||||||
|
|
||||||
Waiting for a thumbs up reaction from the message author: ::
|
Waiting for a thumbs up reaction from the message author: ::
|
||||||
|
|
||||||
@@ -999,7 +997,7 @@ class Client:
|
|||||||
|
|
||||||
# Guild stuff
|
# Guild stuff
|
||||||
|
|
||||||
def fetch_guilds(self, *, limit=100, before=None, after=None):
|
def fetch_guilds(self, *, limit: int = 100, before: SnowflakeTime = None, after: SnowflakeTime = None) -> List[Guild]:
|
||||||
"""Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
"""Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -1078,7 +1076,7 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
code = utils.resolve_template(code)
|
code = utils.resolve_template(code)
|
||||||
data = await self.http.get_template(code)
|
data = await self.http.get_template(code)
|
||||||
return Template(data=data, state=self._connection)
|
return Template(data=data, state=self._connection) # type: ignore
|
||||||
|
|
||||||
async def fetch_guild(self, guild_id):
|
async def fetch_guild(self, guild_id):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@@ -1114,7 +1112,7 @@ class Client:
|
|||||||
data = await self.http.get_guild(guild_id)
|
data = await self.http.get_guild(guild_id)
|
||||||
return Guild(data=data, state=self._connection)
|
return Guild(data=data, state=self._connection)
|
||||||
|
|
||||||
async def create_guild(self, name, region=None, icon=None, *, code=None):
|
async def create_guild(self, name: str, region: Optional[VoiceRegion] = None, icon: Any = None, *, code: str = None):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Creates a :class:`.Guild`.
|
Creates a :class:`.Guild`.
|
||||||
@@ -1161,9 +1159,37 @@ class Client:
|
|||||||
data = await self.http.create_guild(name, region_value, icon)
|
data = await self.http.create_guild(name, region_value, icon)
|
||||||
return Guild(data=data, state=self._connection)
|
return Guild(data=data, state=self._connection)
|
||||||
|
|
||||||
|
async def fetch_stage_instance(self, channel_id: int) -> StageInstance:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Gets a :class:`StageInstance` for a stage channel id.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
channel_id: :class:`int`
|
||||||
|
The stage channel ID.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
:exc:`.NotFound`
|
||||||
|
The stage instance or channel could not be found.
|
||||||
|
:exc:`.HTTPException`
|
||||||
|
Getting the stage instance failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`StageInstance`
|
||||||
|
The stage instance from the stage channel ID.
|
||||||
|
"""
|
||||||
|
data = await self.http.get_stage_instance(channel_id)
|
||||||
|
guild = self.get_guild(int(data['guild_id']))
|
||||||
|
return StageInstance(guild=guild, state=self._connection, data=data) # type: ignore
|
||||||
|
|
||||||
# Invite management
|
# Invite management
|
||||||
|
|
||||||
async def fetch_invite(self, url, *, with_counts=True):
|
async def fetch_invite(self, url: Union[Invite, str], *, with_counts: bool = True, with_expiration: bool = True) -> Invite:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Gets an :class:`.Invite` from a discord.gg URL or ID.
|
Gets an :class:`.Invite` from a discord.gg URL or ID.
|
||||||
@@ -1182,6 +1208,11 @@ class Client:
|
|||||||
Whether to include count information in the invite. This fills the
|
Whether to include count information in the invite. This fills the
|
||||||
:attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count`
|
:attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count`
|
||||||
fields.
|
fields.
|
||||||
|
with_expiration: :class:`bool`
|
||||||
|
Whether to include the expiration date of the invite. This fills the
|
||||||
|
:attr:`.Invite.expires_at` field.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
@@ -1197,10 +1228,10 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
invite_id = utils.resolve_invite(url)
|
invite_id = utils.resolve_invite(url)
|
||||||
data = await self.http.get_invite(invite_id, with_counts=with_counts)
|
data = await self.http.get_invite(invite_id, with_counts=with_counts, with_expiration=with_expiration)
|
||||||
return Invite.from_incomplete(state=self._connection, data=data)
|
return Invite.from_incomplete(state=self._connection, data=data)
|
||||||
|
|
||||||
async def delete_invite(self, invite):
|
async def delete_invite(self, invite: Union[Invite, str]) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Revokes an :class:`.Invite`, URL, or ID to an invite.
|
Revokes an :class:`.Invite`, URL, or ID to an invite.
|
||||||
@@ -1281,14 +1312,13 @@ class Client:
|
|||||||
async def fetch_user(self, user_id):
|
async def fetch_user(self, user_id):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Retrieves a :class:`~discord.User` based on their ID. This can only
|
Retrieves a :class:`~discord.User` based on their ID.
|
||||||
be used by bot accounts. You do not have to share any guilds
|
You do not have to share any guilds with the user to get this information,
|
||||||
with the user to get this information, however many operations
|
however many operations do require that you do.
|
||||||
do require that you do.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This method is an API call. For general usage, consider :meth:`get_user` instead.
|
This method is an API call. If you have :attr:`discord.Intents.members` and member cache enabled, consider :meth:`get_user` instead.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
@@ -1373,3 +1403,68 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
data = await self.http.get_webhook(webhook_id)
|
data = await self.http.get_webhook(webhook_id)
|
||||||
return Webhook.from_state(data, state=self._connection)
|
return Webhook.from_state(data, state=self._connection)
|
||||||
|
|
||||||
|
async def create_dm(self, user):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Creates a :class:`.DMChannel` with this user.
|
||||||
|
|
||||||
|
This should be rarely called, as this is done transparently for most
|
||||||
|
people.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
user: :class:`~discord.abc.Snowflake`
|
||||||
|
The user to create a DM with.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`.DMChannel`
|
||||||
|
The channel that was created.
|
||||||
|
"""
|
||||||
|
state = self._connection
|
||||||
|
found = state._get_private_channel_by_user(user.id)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
|
||||||
|
data = await state.http.start_private_message(user.id)
|
||||||
|
return state.add_dm_channel(data)
|
||||||
|
|
||||||
|
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
|
||||||
|
"""Registers a :class:`~discord.ui.View` for persistent listening.
|
||||||
|
|
||||||
|
This method should be used for when a view is comprised of components
|
||||||
|
that last longer than the lifecycle of the program.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
view: :class:`discord.ui.View`
|
||||||
|
The view to register for dispatching.
|
||||||
|
message_id: Optional[:class:`int`]
|
||||||
|
The message ID that the view is attached to. This is currently used to
|
||||||
|
refresh the view's state during message update events. If not given
|
||||||
|
then message update events are not propagated for the view.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
TypeError
|
||||||
|
A view was not passed.
|
||||||
|
ValueError
|
||||||
|
The view is not persistent. A persistent view has no timeout
|
||||||
|
and all their components have an explicitly provided custom_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(view, View):
|
||||||
|
raise TypeError(f'expected an instance of View not {view.__class__!r}')
|
||||||
|
|
||||||
|
if not view.is_persistent():
|
||||||
|
raise ValueError('View is not persistent. Items need to have a custom_id set and View must have no timeout')
|
||||||
|
|
||||||
|
self._connection.store_view(view, message_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persistent_views(self) -> Sequence[View]:
|
||||||
|
"""Sequence[:class:`View`]: A sequence of persistent views added to the client."""
|
||||||
|
return self._connection.persistent_views
|
||||||
|
|||||||
@@ -25,11 +25,23 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
import colorsys
|
import colorsys
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Colour',
|
'Colour',
|
||||||
'Color',
|
'Color',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CT = TypeVar('CT', bound='Colour')
|
||||||
|
|
||||||
|
|
||||||
class Colour:
|
class Colour:
|
||||||
"""Represents a Discord role colour. This class is similar
|
"""Represents a Discord role colour. This class is similar
|
||||||
to a (red, green, blue) :class:`tuple`.
|
to a (red, green, blue) :class:`tuple`.
|
||||||
@@ -54,6 +66,10 @@ class Colour:
|
|||||||
|
|
||||||
Returns the hex format for the colour.
|
Returns the hex format for the colour.
|
||||||
|
|
||||||
|
.. describe:: int(x)
|
||||||
|
|
||||||
|
Returns the raw colour value.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
------------
|
------------
|
||||||
value: :class:`int`
|
value: :class:`int`
|
||||||
@@ -66,63 +82,66 @@ class Colour:
|
|||||||
if not isinstance(value, int):
|
if not isinstance(value, int):
|
||||||
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
|
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
|
||||||
|
|
||||||
self.value = value
|
self.value: int = value
|
||||||
|
|
||||||
def _get_byte(self, byte):
|
def _get_byte(self, byte: int) -> int:
|
||||||
return (self.value >> (8 * byte)) & 0xff
|
return (self.value >> (8 * byte)) & 0xff
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: Any) -> bool:
|
||||||
return isinstance(other, Colour) and self.value == other.value
|
return isinstance(other, Colour) and self.value == other.value
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other: Any) -> bool:
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f'#{self.value:0>6x}'
|
return f'#{self.value:0>6x}'
|
||||||
|
|
||||||
def __repr__(self):
|
def __int__(self) -> int:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
return f'<Colour value={self.value}>'
|
return f'<Colour value={self.value}>'
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(self.value)
|
return hash(self.value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def r(self):
|
def r(self) -> int:
|
||||||
""":class:`int`: Returns the red component of the colour."""
|
""":class:`int`: Returns the red component of the colour."""
|
||||||
return self._get_byte(2)
|
return self._get_byte(2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def g(self):
|
def g(self) -> int:
|
||||||
""":class:`int`: Returns the green component of the colour."""
|
""":class:`int`: Returns the green component of the colour."""
|
||||||
return self._get_byte(1)
|
return self._get_byte(1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def b(self):
|
def b(self) -> int:
|
||||||
""":class:`int`: Returns the blue component of the colour."""
|
""":class:`int`: Returns the blue component of the colour."""
|
||||||
return self._get_byte(0)
|
return self._get_byte(0)
|
||||||
|
|
||||||
def to_rgb(self):
|
def to_rgb(self) -> Tuple[int, int, int]:
|
||||||
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
|
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
|
||||||
return (self.r, self.g, self.b)
|
return (self.r, self.g, self.b)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_rgb(cls, r, g, b):
|
def from_rgb(cls: Type[CT], r: int, g: int, b: int) -> CT:
|
||||||
"""Constructs a :class:`Colour` from an RGB tuple."""
|
"""Constructs a :class:`Colour` from an RGB tuple."""
|
||||||
return cls((r << 16) + (g << 8) + b)
|
return cls((r << 16) + (g << 8) + b)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_hsv(cls, h, s, v):
|
def from_hsv(cls: Type[CT], h: float, s: float, v: float) -> CT:
|
||||||
"""Constructs a :class:`Colour` from an HSV tuple."""
|
"""Constructs a :class:`Colour` from an HSV tuple."""
|
||||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default(cls):
|
def default(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
||||||
return cls(0)
|
return cls(0)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def random(cls, *, seed=None):
|
def random(cls: Type[CT], *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a random hue.
|
"""A factory method that returns a :class:`Colour` with a random hue.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -143,125 +162,130 @@ class Colour:
|
|||||||
return cls.from_hsv(rand.random(), 1, 1)
|
return cls.from_hsv(rand.random(), 1, 1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def teal(cls):
|
def teal(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
||||||
return cls(0x1abc9c)
|
return cls(0x1abc9c)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_teal(cls):
|
def dark_teal(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
|
||||||
return cls(0x11806a)
|
return cls(0x11806a)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def green(cls):
|
def green(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
|
||||||
return cls(0x2ecc71)
|
return cls(0x2ecc71)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_green(cls):
|
def dark_green(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
|
||||||
return cls(0x1f8b4c)
|
return cls(0x1f8b4c)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def blue(cls):
|
def blue(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
|
||||||
return cls(0x3498db)
|
return cls(0x3498db)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_blue(cls):
|
def dark_blue(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
|
||||||
return cls(0x206694)
|
return cls(0x206694)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def purple(cls):
|
def purple(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
|
||||||
return cls(0x9b59b6)
|
return cls(0x9b59b6)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_purple(cls):
|
def dark_purple(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
|
||||||
return cls(0x71368a)
|
return cls(0x71368a)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def magenta(cls):
|
def magenta(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
|
||||||
return cls(0xe91e63)
|
return cls(0xe91e63)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_magenta(cls):
|
def dark_magenta(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
|
||||||
return cls(0xad1457)
|
return cls(0xad1457)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def gold(cls):
|
def gold(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
|
||||||
return cls(0xf1c40f)
|
return cls(0xf1c40f)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_gold(cls):
|
def dark_gold(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
|
||||||
return cls(0xc27c0e)
|
return cls(0xc27c0e)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def orange(cls):
|
def orange(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
|
||||||
return cls(0xe67e22)
|
return cls(0xe67e22)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_orange(cls):
|
def dark_orange(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
|
||||||
return cls(0xa84300)
|
return cls(0xa84300)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def red(cls):
|
def red(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
||||||
return cls(0xe74c3c)
|
return cls(0xe74c3c)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_red(cls):
|
def dark_red(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
|
||||||
return cls(0x992d22)
|
return cls(0x992d22)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lighter_grey(cls):
|
def lighter_grey(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
|
||||||
return cls(0x95a5a6)
|
return cls(0x95a5a6)
|
||||||
|
|
||||||
lighter_gray = lighter_grey
|
lighter_gray = lighter_grey
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_grey(cls):
|
def dark_grey(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
|
||||||
return cls(0x607d8b)
|
return cls(0x607d8b)
|
||||||
|
|
||||||
dark_gray = dark_grey
|
dark_gray = dark_grey
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def light_grey(cls):
|
def light_grey(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
|
||||||
return cls(0x979c9f)
|
return cls(0x979c9f)
|
||||||
|
|
||||||
light_gray = light_grey
|
light_gray = light_grey
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def darker_grey(cls):
|
def darker_grey(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
|
||||||
return cls(0x546e7a)
|
return cls(0x546e7a)
|
||||||
|
|
||||||
darker_gray = darker_grey
|
darker_gray = darker_grey
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def blurple(cls):
|
def og_blurple(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
|
||||||
return cls(0x7289da)
|
return cls(0x7289da)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def greyple(cls):
|
def blurple(cls: Type[CT]) -> CT:
|
||||||
|
"""A factory method that returns a :class:`Colour` with a value of ``0x5865F2``."""
|
||||||
|
return cls(0x5865F2)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def greyple(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
|
||||||
return cls(0x99aab5)
|
return cls(0x99aab5)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_theme(cls):
|
def dark_theme(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x36393F``.
|
"""A factory method that returns a :class:`Colour` with a value of ``0x36393F``.
|
||||||
This will appear transparent on Discord's dark theme.
|
This will appear transparent on Discord's dark theme.
|
||||||
|
|
||||||
@@ -269,4 +293,21 @@ class Colour:
|
|||||||
"""
|
"""
|
||||||
return cls(0x36393F)
|
return cls(0x36393F)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fuchsia(cls: Type[CT]) -> CT:
|
||||||
|
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459E``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return cls(0xEB459E)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def yellow(cls: Type[CT]) -> CT:
|
||||||
|
"""A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return cls(0xFEE75C)
|
||||||
|
|
||||||
|
|
||||||
Color = Colour
|
Color = Colour
|
||||||
|
|||||||
358
discord/components.py
Normal file
358
discord/components.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||||
|
from .enums import try_enum, ComponentType, ButtonStyle
|
||||||
|
from .utils import get_slots, MISSING
|
||||||
|
from .partial_emoji import PartialEmoji, _EmojiTag
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.components import (
|
||||||
|
Component as ComponentPayload,
|
||||||
|
ButtonComponent as ButtonComponentPayload,
|
||||||
|
SelectMenu as SelectMenuPayload,
|
||||||
|
SelectOption as SelectOptionPayload,
|
||||||
|
ActionRow as ActionRowPayload,
|
||||||
|
)
|
||||||
|
from .emoji import Emoji
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Component',
|
||||||
|
'ActionRow',
|
||||||
|
'Button',
|
||||||
|
'SelectMenu',
|
||||||
|
'SelectOption',
|
||||||
|
)
|
||||||
|
|
||||||
|
C = TypeVar('C', bound='Component')
|
||||||
|
|
||||||
|
|
||||||
|
class Component:
|
||||||
|
"""Represents a Discord Bot UI Kit Component.
|
||||||
|
|
||||||
|
Currently, the only components supported by Discord are:
|
||||||
|
|
||||||
|
- :class:`ActionRow`
|
||||||
|
- :class:`Button`
|
||||||
|
- :class:`SelectMenu`
|
||||||
|
|
||||||
|
This class is abstract and cannot be instantiated.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
type: :class:`ComponentType`
|
||||||
|
The type of component.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = ('type',)
|
||||||
|
|
||||||
|
__repr_info__: ClassVar[Tuple[str, ...]]
|
||||||
|
type: ComponentType
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__repr_info__)
|
||||||
|
return f'<{self.__class__.__name__} {attrs}>'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _raw_construct(cls: Type[C], **kwargs) -> C:
|
||||||
|
self: C = cls.__new__(cls)
|
||||||
|
for slot in get_slots(cls):
|
||||||
|
try:
|
||||||
|
value = kwargs[slot]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
setattr(self, slot, value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ActionRow(Component):
|
||||||
|
"""Represents a Discord Bot UI Kit Action Row.
|
||||||
|
|
||||||
|
This is a component that holds up to 5 children components in a row.
|
||||||
|
|
||||||
|
This inherits from :class:`Component`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
type: :class:`ComponentType`
|
||||||
|
The type of component.
|
||||||
|
children: List[:class:`Component`]
|
||||||
|
The children components that this holds, if any.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = ('children',)
|
||||||
|
|
||||||
|
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||||
|
|
||||||
|
def __init__(self, data: ComponentPayload):
|
||||||
|
self.type: ComponentType = try_enum(ComponentType, data['type'])
|
||||||
|
self.children: List[Component] = [_component_factory(d) for d in data.get('components', [])]
|
||||||
|
|
||||||
|
def to_dict(self) -> ActionRowPayload:
|
||||||
|
return {
|
||||||
|
'type': int(self.type),
|
||||||
|
'components': [child.to_dict() for child in self.children],
|
||||||
|
} # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
"""Represents a button from the Discord Bot UI Kit.
|
||||||
|
|
||||||
|
This inherits from :class:`Component`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
style: :class:`ComponentButtonStyle`
|
||||||
|
The style of the button.
|
||||||
|
custom_id: Optional[:class:`str`]
|
||||||
|
The ID of the button that gets received during an interaction.
|
||||||
|
If this button is for a URL, it does not have a custom ID.
|
||||||
|
url: Optional[:class:`str`]
|
||||||
|
The URL this button sends you to.
|
||||||
|
disabled: :class:`bool`
|
||||||
|
Whether the button is disabled or not.
|
||||||
|
label: Optional[:class:`str`]
|
||||||
|
The label of the button, if any.
|
||||||
|
emoji: Optional[:class:`PartialEmoji`]
|
||||||
|
The emoji of the button, if available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
|
'style',
|
||||||
|
'custom_id',
|
||||||
|
'url',
|
||||||
|
'disabled',
|
||||||
|
'label',
|
||||||
|
'emoji',
|
||||||
|
)
|
||||||
|
|
||||||
|
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||||
|
|
||||||
|
def __init__(self, data: ButtonComponentPayload):
|
||||||
|
self.type: ComponentType = try_enum(ComponentType, data['type'])
|
||||||
|
self.style: ButtonStyle = try_enum(ButtonStyle, data['style'])
|
||||||
|
self.custom_id: Optional[str] = data.get('custom_id')
|
||||||
|
self.url: Optional[str] = data.get('url')
|
||||||
|
self.disabled: bool = data.get('disabled', False)
|
||||||
|
self.label: Optional[str] = data.get('label')
|
||||||
|
self.emoji: Optional[PartialEmoji]
|
||||||
|
try:
|
||||||
|
self.emoji = PartialEmoji.from_dict(data['emoji'])
|
||||||
|
except KeyError:
|
||||||
|
self.emoji = None
|
||||||
|
|
||||||
|
def to_dict(self) -> ButtonComponentPayload:
|
||||||
|
payload = {
|
||||||
|
'type': 2,
|
||||||
|
'style': int(self.style),
|
||||||
|
'label': self.label,
|
||||||
|
'disabled': self.disabled,
|
||||||
|
}
|
||||||
|
if self.custom_id:
|
||||||
|
payload['custom_id'] = self.custom_id
|
||||||
|
|
||||||
|
if self.url:
|
||||||
|
payload['url'] = self.url
|
||||||
|
|
||||||
|
if self.emoji:
|
||||||
|
payload['emoji'] = self.emoji.to_dict()
|
||||||
|
|
||||||
|
return payload # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SelectMenu(Component):
|
||||||
|
"""Represents a select menu from the Discord Bot UI Kit.
|
||||||
|
|
||||||
|
A select menu is functionally the same as a dropdown, however
|
||||||
|
on mobile it renders a bit differently.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
custom_id: Optional[:class:`str`]
|
||||||
|
The ID of the select menu that gets received during an interaction.
|
||||||
|
placeholder: Optional[:class:`str`]
|
||||||
|
The placeholder text that is shown if nothing is selected, if any.
|
||||||
|
min_values: :class:`int`
|
||||||
|
The minimum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
max_values: :class:`int`
|
||||||
|
The maximum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
options: List[:class:`SelectOption`]
|
||||||
|
A list of options that can be selected in this menu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
|
'custom_id',
|
||||||
|
'placeholder',
|
||||||
|
'min_values',
|
||||||
|
'max_values',
|
||||||
|
'options',
|
||||||
|
)
|
||||||
|
|
||||||
|
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||||
|
|
||||||
|
def __init__(self, data: SelectMenuPayload):
|
||||||
|
self.type = ComponentType.select
|
||||||
|
self.custom_id: str = data['custom_id']
|
||||||
|
self.placeholder: Optional[str] = data.get('placeholder')
|
||||||
|
self.min_values: int = data.get('min_values', 1)
|
||||||
|
self.max_values: int = data.get('max_values', 1)
|
||||||
|
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])]
|
||||||
|
|
||||||
|
def to_dict(self) -> SelectMenuPayload:
|
||||||
|
payload: SelectMenuPayload = {
|
||||||
|
'type': self.type.value,
|
||||||
|
'custom_id': self.custom_id,
|
||||||
|
'min_values': self.min_values,
|
||||||
|
'max_values': self.max_values,
|
||||||
|
'options': [op.to_dict() for op in self.options],
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.placeholder:
|
||||||
|
payload['placeholder'] = self.placeholder
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class SelectOption:
|
||||||
|
"""Represents a select menu's option.
|
||||||
|
|
||||||
|
These can be created by users.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
label: :class:`str`
|
||||||
|
The label of the option. This is displayed to users.
|
||||||
|
Can only be up to 25 characters.
|
||||||
|
value: :class:`str`
|
||||||
|
The value of the option. This is not displayed to users.
|
||||||
|
If not provided when constructed then it defaults to the
|
||||||
|
label. Can only be up to 100 characters.
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
An additional description of the option, if any.
|
||||||
|
Can only be up to 50 characters.
|
||||||
|
emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
|
||||||
|
The emoji of the option, if available.
|
||||||
|
default: :class:`bool`
|
||||||
|
Whether this option is selected by default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
|
'label',
|
||||||
|
'value',
|
||||||
|
'description',
|
||||||
|
'emoji',
|
||||||
|
'default',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
value: str = MISSING,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||||
|
default: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.label = label
|
||||||
|
self.value = label if value is MISSING else value
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
if emoji is not None:
|
||||||
|
if isinstance(emoji, str):
|
||||||
|
emoji = PartialEmoji.from_str(emoji)
|
||||||
|
elif isinstance(emoji, _EmojiTag):
|
||||||
|
emoji = emoji._to_partial()
|
||||||
|
else:
|
||||||
|
raise TypeError(f'expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}')
|
||||||
|
|
||||||
|
self.emoji = emoji
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'<SelectOption label={self.label!r} value={self.value!r} description={self.description!r} '
|
||||||
|
f'emoji={self.emoji!r} default={self.default!r}>'
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: SelectOptionPayload) -> SelectOption:
|
||||||
|
try:
|
||||||
|
emoji = PartialEmoji.from_dict(data['emoji'])
|
||||||
|
except KeyError:
|
||||||
|
emoji = None
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
label=data['label'],
|
||||||
|
value=data['value'],
|
||||||
|
description=data.get('description'),
|
||||||
|
emoji=emoji,
|
||||||
|
default=data.get('default', False),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> SelectOptionPayload:
|
||||||
|
payload: SelectOptionPayload = {
|
||||||
|
'label': self.label,
|
||||||
|
'value': self.value,
|
||||||
|
'default': self.default,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.emoji:
|
||||||
|
payload['emoji'] = self.emoji.to_dict() # type: ignore
|
||||||
|
|
||||||
|
if self.description:
|
||||||
|
payload['description'] = self.description
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _component_factory(data: ComponentPayload) -> Component:
|
||||||
|
component_type = data['type']
|
||||||
|
if component_type == 1:
|
||||||
|
return ActionRow(data)
|
||||||
|
elif component_type == 2:
|
||||||
|
return Button(data) # type: ignore
|
||||||
|
elif component_type == 3:
|
||||||
|
return SelectMenu(data) # type: ignore
|
||||||
|
else:
|
||||||
|
as_enum = try_enum(ComponentType, component_type)
|
||||||
|
return Component._raw_construct(type=as_enum)
|
||||||
@@ -179,10 +179,10 @@ class Embed:
|
|||||||
*,
|
*,
|
||||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||||
title: MaybeEmpty[str] = EmptyEmbed,
|
title: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
type: EmbedType = 'rich',
|
type: EmbedType = 'rich',
|
||||||
url: MaybeEmpty[str] = EmptyEmbed,
|
url: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
description: MaybeEmpty[str] = EmptyEmbed,
|
description: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
timestamp: datetime.datetime = None,
|
timestamp: datetime.datetime = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -273,11 +273,11 @@ class Embed:
|
|||||||
total += len(field['name']) + len(field['value'])
|
total += len(field['name']) + len(field['value'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
footer = self._footer
|
footer_text = self._footer['text']
|
||||||
except AttributeError:
|
except (AttributeError, KeyError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
total += len(footer['text'])
|
total += len(footer_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
author = self._author
|
author = self._author
|
||||||
@@ -342,7 +342,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
|
||||||
|
|
||||||
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
def set_footer(self: E, *, text: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> E:
|
||||||
"""Sets the footer for the embed content.
|
"""Sets the footer for the embed content.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -365,6 +365,21 @@ class Embed:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def remove_footer(self: E) -> E:
|
||||||
|
"""Clears embed's footer information.
|
||||||
|
|
||||||
|
This function returns the class instance to allow for fluent-style
|
||||||
|
chaining.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
del self._footer
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> _EmbedMediaProxy:
|
def image(self) -> _EmbedMediaProxy:
|
||||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||||
@@ -380,7 +395,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
||||||
|
|
||||||
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
|
def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||||
"""Sets the image for the embed content.
|
"""Sets the image for the embed content.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -422,7 +437,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||||
|
|
||||||
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
|
def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||||
"""Sets the thumbnail for the embed content.
|
"""Sets the thumbnail for the embed content.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -483,7 +498,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
|
||||||
|
|
||||||
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
def set_author(self: E, *, name: Any, url: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> E:
|
||||||
"""Sets the author for the embed content.
|
"""Sets the author for the embed content.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -528,7 +543,7 @@ class Embed:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def fields(self) -> List[_EmbedFieldProxy]:
|
def fields(self) -> List[_EmbedFieldProxy]:
|
||||||
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
||||||
|
|
||||||
See :meth:`add_field` for possible values you can access.
|
See :meth:`add_field` for possible values you can access.
|
||||||
|
|
||||||
@@ -536,7 +551,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
|
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
|
||||||
|
|
||||||
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
|
def add_field(self: E, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||||
"""Adds a field to the embed object.
|
"""Adds a field to the embed object.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -565,7 +580,7 @@ class Embed:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
def insert_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||||
"""Inserts a field before a specified index to the embed.
|
"""Inserts a field before a specified index to the embed.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@@ -626,7 +641,7 @@ class Embed:
|
|||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
def set_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||||
"""Modifies a field to the embed object.
|
"""Modifies a field to the embed object.
|
||||||
|
|
||||||
The index must point to a valid pre-existing field.
|
The index must point to a valid pre-existing field.
|
||||||
|
|||||||
156
discord/emoji.py
156
discord/emoji.py
@@ -22,16 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .asset import Asset
|
from __future__ import annotations
|
||||||
from . import utils
|
from typing import Any, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||||
from .partial_emoji import _EmojiTag
|
|
||||||
|
from .asset import Asset, AssetMixin
|
||||||
|
from .utils import SnowflakeList, snowflake_time, MISSING
|
||||||
|
from .partial_emoji import _EmojiTag, PartialEmoji
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Emoji',
|
'Emoji',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Emoji(_EmojiTag):
|
if TYPE_CHECKING:
|
||||||
|
from .types.emoji import Emoji as EmojiPayload
|
||||||
|
from .guild import Guild
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .abc import Snowflake
|
||||||
|
from .role import Role
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Emoji(_EmojiTag, AssetMixin):
|
||||||
"""Represents a custom emoji.
|
"""Represents a custom emoji.
|
||||||
|
|
||||||
Depending on the way this object was created, some of the attributes can
|
Depending on the way this object was created, some of the attributes can
|
||||||
@@ -80,68 +92,76 @@ class Emoji(_EmojiTag):
|
|||||||
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
|
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
|
||||||
having the :attr:`~Permissions.manage_emojis` permission.
|
having the :attr:`~Permissions.manage_emojis` permission.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('require_colons', 'animated', 'managed', 'id', 'name', '_roles', 'guild_id',
|
|
||||||
'_state', 'user', 'available')
|
|
||||||
|
|
||||||
def __init__(self, *, guild, state, data):
|
__slots__: Tuple[str, ...] = (
|
||||||
self.guild_id = guild.id
|
'require_colons',
|
||||||
self._state = state
|
'animated',
|
||||||
|
'managed',
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'_roles',
|
||||||
|
'guild_id',
|
||||||
|
'_state',
|
||||||
|
'user',
|
||||||
|
'available',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload):
|
||||||
|
self.guild_id: int = guild.id
|
||||||
|
self._state: ConnectionState = state
|
||||||
self._from_data(data)
|
self._from_data(data)
|
||||||
|
|
||||||
def _from_data(self, emoji):
|
def _from_data(self, emoji: EmojiPayload):
|
||||||
self.require_colons = emoji['require_colons']
|
self.require_colons: bool = emoji.get('require_colons', False)
|
||||||
self.managed = emoji['managed']
|
self.managed: bool = emoji.get('managed', False)
|
||||||
self.id = int(emoji['id'])
|
self.id: int = int(emoji['id']) # type: ignore
|
||||||
self.name = emoji['name']
|
self.name: str = emoji['name'] # type: ignore
|
||||||
self.animated = emoji.get('animated', False)
|
self.animated: bool = emoji.get('animated', False)
|
||||||
self.available = emoji.get('available', True)
|
self.available: bool = emoji.get('available', True)
|
||||||
self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))
|
self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get('roles', [])))
|
||||||
user = emoji.get('user')
|
user = emoji.get('user')
|
||||||
self.user = User(state=self._state, data=user) if user else None
|
self.user: Optional[User] = User(state=self._state, data=user) if user else None
|
||||||
|
|
||||||
def _iterator(self):
|
def _to_partial(self) -> PartialEmoji:
|
||||||
|
return PartialEmoji(name=self.name, animated=self.animated, id=self.id)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||||
for attr in self.__slots__:
|
for attr in self.__slots__:
|
||||||
if attr[0] != '_':
|
if attr[0] != '_':
|
||||||
value = getattr(self, attr, None)
|
value = getattr(self, attr, None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
yield (attr, value)
|
yield (attr, value)
|
||||||
|
|
||||||
def __iter__(self):
|
def __str__(self) -> str:
|
||||||
return self._iterator()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.animated:
|
if self.animated:
|
||||||
return '<a:{0.name}:{0.id}>'.format(self)
|
return f'<a:{self.name}:{self.id}>'
|
||||||
return "<:{0.name}:{0.id}>".format(self)
|
return f'<:{self.name}:{self.id}>'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Emoji id={0.id} name={0.name!r} animated={0.animated} managed={0.managed}>'.format(self)
|
return f'<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: Any) -> bool:
|
||||||
return isinstance(other, _EmojiTag) and self.id == other.id
|
return isinstance(other, _EmojiTag) and self.id == other.id
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other: Any) -> bool:
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return self.id >> 22
|
return self.id >> 22
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime:
|
||||||
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
|
||||||
return utils.snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
""":class:`Asset`: Returns the asset of the emoji.
|
""":class:`str`: Returns the URL of the emoji."""
|
||||||
|
fmt = 'gif' if self.animated else 'png'
|
||||||
This is equivalent to calling :meth:`url_as` with
|
return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
|
||||||
the default parameters (i.e. png/gif detection).
|
|
||||||
"""
|
|
||||||
return self.url_as(format=None)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def roles(self):
|
def roles(self) -> List[Role]:
|
||||||
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
|
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
|
||||||
|
|
||||||
If roles is empty, the emoji is unrestricted.
|
If roles is empty, the emoji is unrestricted.
|
||||||
@@ -153,44 +173,11 @@ class Emoji(_EmojiTag):
|
|||||||
return [role for role in guild.roles if self._roles.has(role.id)]
|
return [role for role in guild.roles if self._roles.has(role.id)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def guild(self):
|
def guild(self) -> Guild:
|
||||||
""":class:`Guild`: The guild this emoji belongs to."""
|
""":class:`Guild`: The guild this emoji belongs to."""
|
||||||
return self._state._get_guild(self.guild_id)
|
return self._state._get_guild(self.guild_id)
|
||||||
|
|
||||||
|
def is_usable(self) -> bool:
|
||||||
def url_as(self, *, format=None, static_format="png"):
|
|
||||||
"""Returns an :class:`Asset` for the emoji's url.
|
|
||||||
|
|
||||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
|
||||||
'gif' is only valid for animated emojis.
|
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
format: Optional[:class:`str`]
|
|
||||||
The format to attempt to convert the emojis to.
|
|
||||||
If the format is ``None``, then it is automatically
|
|
||||||
detected as either 'gif' or static_format, depending on whether the
|
|
||||||
emoji is animated or not.
|
|
||||||
static_format: Optional[:class:`str`]
|
|
||||||
Format to attempt to convert only non-animated emoji's to.
|
|
||||||
Defaults to 'png'
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or ``static_format``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
|
||||||
|
|
||||||
|
|
||||||
def is_usable(self):
|
|
||||||
""":class:`bool`: Whether the bot can use this emoji.
|
""":class:`bool`: Whether the bot can use this emoji.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
@@ -202,7 +189,7 @@ class Emoji(_EmojiTag):
|
|||||||
emoji_roles, my_roles = self._roles, self.guild.me._roles
|
emoji_roles, my_roles = self._roles, self.guild.me._roles
|
||||||
return any(my_roles.has(role_id) for role_id in emoji_roles)
|
return any(my_roles.has(role_id) for role_id in emoji_roles)
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Deletes the custom emoji.
|
Deletes the custom emoji.
|
||||||
@@ -225,7 +212,7 @@ class Emoji(_EmojiTag):
|
|||||||
|
|
||||||
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
||||||
|
|
||||||
async def edit(self, *, name=None, roles=None, reason=None):
|
async def edit(self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None) -> None:
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
Edits the custom emoji.
|
Edits the custom emoji.
|
||||||
@@ -237,8 +224,8 @@ class Emoji(_EmojiTag):
|
|||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The new emoji name.
|
The new emoji name.
|
||||||
roles: Optional[list[:class:`Role`]]
|
roles: Optional[List[:class:`~discord.abc.Snowflake`]]
|
||||||
A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone.
|
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
|
||||||
reason: Optional[:class:`str`]
|
reason: Optional[:class:`str`]
|
||||||
The reason for editing this emoji. Shows up on the audit log.
|
The reason for editing this emoji. Shows up on the audit log.
|
||||||
|
|
||||||
@@ -250,7 +237,10 @@ class Emoji(_EmojiTag):
|
|||||||
An error occurred editing the emoji.
|
An error occurred editing the emoji.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = name or self.name
|
payload = {}
|
||||||
if roles:
|
if name is not MISSING:
|
||||||
roles = [role.id for role in roles]
|
payload['name'] = name
|
||||||
await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason)
|
if roles is not MISSING:
|
||||||
|
payload['roles'] = [role.id for role in roles]
|
||||||
|
|
||||||
|
await self._state.http.edit_custom_emoji(self.guild.id, self.id, payload=payload, reason=reason)
|
||||||
|
|||||||
194
discord/enums.py
194
discord/enums.py
@@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
import types
|
import types
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Any, TYPE_CHECKING, Type, TypeVar
|
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Enum',
|
'Enum',
|
||||||
@@ -46,6 +46,14 @@ __all__ = (
|
|||||||
'ExpireBehaviour',
|
'ExpireBehaviour',
|
||||||
'ExpireBehavior',
|
'ExpireBehavior',
|
||||||
'StickerType',
|
'StickerType',
|
||||||
|
'InviteTarget',
|
||||||
|
'VideoQualityMode',
|
||||||
|
'ComponentType',
|
||||||
|
'ButtonStyle',
|
||||||
|
'StagePrivacyLevel',
|
||||||
|
'InteractionType',
|
||||||
|
'InteractionResponseType',
|
||||||
|
'NSFWLevel',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_value_cls(name):
|
def _create_value_cls(name):
|
||||||
@@ -147,14 +155,17 @@ else:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
class ChannelType(Enum):
|
class ChannelType(Enum):
|
||||||
text = 0
|
text = 0
|
||||||
private = 1
|
private = 1
|
||||||
voice = 2
|
voice = 2
|
||||||
group = 3
|
group = 3
|
||||||
category = 4
|
category = 4
|
||||||
news = 5
|
news = 5
|
||||||
store = 6
|
store = 6
|
||||||
stage_voice = 13
|
news_thread = 10
|
||||||
|
public_thread = 11
|
||||||
|
private_thread = 12
|
||||||
|
stage_voice = 13
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -178,6 +189,11 @@ class MessageType(Enum):
|
|||||||
guild_discovery_requalified = 15
|
guild_discovery_requalified = 15
|
||||||
guild_discovery_grace_period_initial_warning = 16
|
guild_discovery_grace_period_initial_warning = 16
|
||||||
guild_discovery_grace_period_final_warning = 17
|
guild_discovery_grace_period_final_warning = 17
|
||||||
|
thread_created = 18
|
||||||
|
reply = 19
|
||||||
|
application_command = 20
|
||||||
|
thread_starter_message = 21
|
||||||
|
guild_invite_reminder = 22
|
||||||
|
|
||||||
class VoiceRegion(Enum):
|
class VoiceRegion(Enum):
|
||||||
us_west = 'us-west'
|
us_west = 'us-west'
|
||||||
@@ -220,14 +236,11 @@ class SpeakingState(Enum):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
class VerificationLevel(Enum):
|
class VerificationLevel(Enum):
|
||||||
none = 0
|
none = 0
|
||||||
low = 1
|
low = 1
|
||||||
medium = 2
|
medium = 2
|
||||||
high = 3
|
high = 3
|
||||||
table_flip = 3
|
highest = 4
|
||||||
extreme = 4
|
|
||||||
double_table_flip = 4
|
|
||||||
very_high = 4
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -307,50 +320,56 @@ class AuditLogAction(Enum):
|
|||||||
integration_create = 80
|
integration_create = 80
|
||||||
integration_update = 81
|
integration_update = 81
|
||||||
integration_delete = 82
|
integration_delete = 82
|
||||||
|
stage_instance_create = 83
|
||||||
|
stage_instance_update = 84
|
||||||
|
stage_instance_delete = 85
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def category(self):
|
def category(self) -> Optional[AuditLogActionCategory]:
|
||||||
lookup = {
|
lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = {
|
||||||
AuditLogAction.guild_update: AuditLogActionCategory.update,
|
AuditLogAction.guild_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.channel_create: AuditLogActionCategory.create,
|
AuditLogAction.channel_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.channel_update: AuditLogActionCategory.update,
|
AuditLogAction.channel_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
|
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
|
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
|
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
|
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.kick: None,
|
AuditLogAction.kick: None,
|
||||||
AuditLogAction.member_prune: None,
|
AuditLogAction.member_prune: None,
|
||||||
AuditLogAction.ban: None,
|
AuditLogAction.ban: None,
|
||||||
AuditLogAction.unban: None,
|
AuditLogAction.unban: None,
|
||||||
AuditLogAction.member_update: AuditLogActionCategory.update,
|
AuditLogAction.member_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.member_role_update: AuditLogActionCategory.update,
|
AuditLogAction.member_role_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.member_move: None,
|
AuditLogAction.member_move: None,
|
||||||
AuditLogAction.member_disconnect: None,
|
AuditLogAction.member_disconnect: None,
|
||||||
AuditLogAction.bot_add: None,
|
AuditLogAction.bot_add: None,
|
||||||
AuditLogAction.role_create: AuditLogActionCategory.create,
|
AuditLogAction.role_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.role_update: AuditLogActionCategory.update,
|
AuditLogAction.role_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.role_delete: AuditLogActionCategory.delete,
|
AuditLogAction.role_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.invite_create: AuditLogActionCategory.create,
|
AuditLogAction.invite_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.invite_update: AuditLogActionCategory.update,
|
AuditLogAction.invite_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
|
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.webhook_create: AuditLogActionCategory.create,
|
AuditLogAction.webhook_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.webhook_update: AuditLogActionCategory.update,
|
AuditLogAction.webhook_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
|
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.emoji_create: AuditLogActionCategory.create,
|
AuditLogAction.emoji_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.emoji_update: AuditLogActionCategory.update,
|
AuditLogAction.emoji_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
|
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.message_delete: AuditLogActionCategory.delete,
|
AuditLogAction.message_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
|
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
|
||||||
AuditLogAction.message_pin: None,
|
AuditLogAction.message_pin: None,
|
||||||
AuditLogAction.message_unpin: None,
|
AuditLogAction.message_unpin: None,
|
||||||
AuditLogAction.integration_create: AuditLogActionCategory.create,
|
AuditLogAction.integration_create: AuditLogActionCategory.create,
|
||||||
AuditLogAction.integration_update: AuditLogActionCategory.update,
|
AuditLogAction.integration_update: AuditLogActionCategory.update,
|
||||||
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
|
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
|
||||||
|
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
|
||||||
|
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
|
||||||
|
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
|
||||||
}
|
}
|
||||||
return lookup[self]
|
return lookup[self]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_type(self):
|
def target_type(self) -> Optional[str]:
|
||||||
v = self.value
|
v = self.value
|
||||||
if v == -1:
|
if v == -1:
|
||||||
return 'all'
|
return 'all'
|
||||||
@@ -368,10 +387,14 @@ class AuditLogAction(Enum):
|
|||||||
return 'webhook'
|
return 'webhook'
|
||||||
elif v < 70:
|
elif v < 70:
|
||||||
return 'emoji'
|
return 'emoji'
|
||||||
|
elif v == 73:
|
||||||
|
return 'channel'
|
||||||
elif v < 80:
|
elif v < 80:
|
||||||
return 'message'
|
return 'message'
|
||||||
elif v < 90:
|
elif v < 83:
|
||||||
return 'integration'
|
return 'integration'
|
||||||
|
elif v < 90:
|
||||||
|
return 'stage_instance'
|
||||||
|
|
||||||
class UserFlags(Enum):
|
class UserFlags(Enum):
|
||||||
staff = 1
|
staff = 1
|
||||||
@@ -390,6 +413,7 @@ class UserFlags(Enum):
|
|||||||
bug_hunter_level_2 = 16384
|
bug_hunter_level_2 = 16384
|
||||||
verified_bot = 65536
|
verified_bot = 65536
|
||||||
verified_bot_developer = 131072
|
verified_bot_developer = 131072
|
||||||
|
discord_certified_moderator = 262144
|
||||||
|
|
||||||
class ActivityType(Enum):
|
class ActivityType(Enum):
|
||||||
unknown = -1
|
unknown = -1
|
||||||
@@ -410,6 +434,7 @@ class TeamMembershipState(Enum):
|
|||||||
class WebhookType(Enum):
|
class WebhookType(Enum):
|
||||||
incoming = 1
|
incoming = 1
|
||||||
channel_follower = 2
|
channel_follower = 2
|
||||||
|
application = 3
|
||||||
|
|
||||||
class ExpireBehaviour(Enum):
|
class ExpireBehaviour(Enum):
|
||||||
remove_role = 0
|
remove_role = 0
|
||||||
@@ -422,9 +447,66 @@ class StickerType(Enum):
|
|||||||
apng = 2
|
apng = 2
|
||||||
lottie = 3
|
lottie = 3
|
||||||
|
|
||||||
|
class InviteTarget(Enum):
|
||||||
|
unknown = 0
|
||||||
|
stream = 1
|
||||||
|
embedded_application = 2
|
||||||
|
|
||||||
class InteractionType(Enum):
|
class InteractionType(Enum):
|
||||||
ping = 1
|
ping = 1
|
||||||
application_command = 2
|
application_command = 2
|
||||||
|
component = 3
|
||||||
|
|
||||||
|
class InteractionResponseType(Enum):
|
||||||
|
pong = 1
|
||||||
|
# ack = 2 (deprecated)
|
||||||
|
# channel_message = 3 (deprecated)
|
||||||
|
channel_message = 4 # (with source)
|
||||||
|
deferred_channel_message = 5 # (with source)
|
||||||
|
deferred_message_update = 6 # for components
|
||||||
|
message_update = 7 # for components
|
||||||
|
|
||||||
|
class VideoQualityMode(Enum):
|
||||||
|
auto = 1
|
||||||
|
full = 2
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class ComponentType(Enum):
|
||||||
|
action_row = 1
|
||||||
|
button = 2
|
||||||
|
select = 3
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class ButtonStyle(Enum):
|
||||||
|
primary = 1
|
||||||
|
secondary = 2
|
||||||
|
success = 3
|
||||||
|
danger = 4
|
||||||
|
link = 5
|
||||||
|
|
||||||
|
# Aliases
|
||||||
|
blurple = 1
|
||||||
|
grey = 2
|
||||||
|
green = 3
|
||||||
|
red = 4
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class StagePrivacyLevel(Enum):
|
||||||
|
public = 1
|
||||||
|
closed = 2
|
||||||
|
guild_only = 2
|
||||||
|
|
||||||
|
class NSFWLevel(Enum):
|
||||||
|
default = 0
|
||||||
|
explicit = 1
|
||||||
|
safe = 2
|
||||||
|
age_restricted = 3
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ from .help import *
|
|||||||
from .converter import *
|
from .converter import *
|
||||||
from .cooldowns import *
|
from .cooldowns import *
|
||||||
from .cog import *
|
from .cog import *
|
||||||
|
from .flags import *
|
||||||
|
|||||||
@@ -121,11 +121,6 @@ class BotBase(GroupMixin):
|
|||||||
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
||||||
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
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
|
|
||||||
else:
|
|
||||||
self._skip_check = lambda x, y: x == y
|
|
||||||
|
|
||||||
if help_command is _default:
|
if help_command is _default:
|
||||||
self.help_command = DefaultHelpCommand()
|
self.help_command = DefaultHelpCommand()
|
||||||
else:
|
else:
|
||||||
@@ -167,11 +162,12 @@ class BotBase(GroupMixin):
|
|||||||
if self.extra_events.get('on_command_error', None):
|
if self.extra_events.get('on_command_error', None):
|
||||||
return
|
return
|
||||||
|
|
||||||
if hasattr(context.command, 'on_error'):
|
command = context.command
|
||||||
|
if command and command.has_error_handler():
|
||||||
return
|
return
|
||||||
|
|
||||||
cog = context.cog
|
cog = context.cog
|
||||||
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
|
if cog and cog.has_error_handler():
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
|
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
|
||||||
@@ -219,7 +215,7 @@ class BotBase(GroupMixin):
|
|||||||
The function that was used as a global check.
|
The function that was used as a global check.
|
||||||
call_once: :class:`bool`
|
call_once: :class:`bool`
|
||||||
If the function should only be called once per
|
If the function should only be called once per
|
||||||
:meth:`.Command.invoke` call.
|
:meth:`.invoke` call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if call_once:
|
if call_once:
|
||||||
@@ -252,7 +248,7 @@ class BotBase(GroupMixin):
|
|||||||
r"""A decorator that adds a "call once" global check to the bot.
|
r"""A decorator that adds a "call once" global check to the bot.
|
||||||
|
|
||||||
Unlike regular global checks, this one is called only once
|
Unlike regular global checks, this one is called only once
|
||||||
per :meth:`.Command.invoke` call.
|
per :meth:`.invoke` call.
|
||||||
|
|
||||||
Regular global checks are called whenever a command is called
|
Regular global checks are called whenever a command is called
|
||||||
or :meth:`.Command.can_run` is called. This type of check
|
or :meth:`.Command.can_run` is called. This type of check
|
||||||
@@ -488,15 +484,25 @@ class BotBase(GroupMixin):
|
|||||||
|
|
||||||
# cogs
|
# cogs
|
||||||
|
|
||||||
def add_cog(self, cog):
|
def add_cog(self, cog: Cog, *, override: bool = False) -> None:
|
||||||
"""Adds a "cog" to the bot.
|
"""Adds a "cog" to the bot.
|
||||||
|
|
||||||
A cog is a class that has its own event listeners and commands.
|
A cog is a class that has its own event listeners and commands.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
|
:exc:`.ClientException` is raised when a cog with the same name
|
||||||
|
is already loaded.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
cog: :class:`.Cog`
|
cog: :class:`.Cog`
|
||||||
The cog to register to the bot.
|
The cog to register to the bot.
|
||||||
|
override: :class:`bool`
|
||||||
|
If a previously loaded cog with the same name should be ejected
|
||||||
|
instead of raising an error.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
@@ -504,13 +510,23 @@ class BotBase(GroupMixin):
|
|||||||
The cog does not inherit from :class:`.Cog`.
|
The cog does not inherit from :class:`.Cog`.
|
||||||
CommandError
|
CommandError
|
||||||
An error happened during loading.
|
An error happened during loading.
|
||||||
|
.ClientException
|
||||||
|
A cog with the same name is already loaded.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(cog, Cog):
|
if not isinstance(cog, Cog):
|
||||||
raise TypeError('cogs must derive from Cog')
|
raise TypeError('cogs must derive from Cog')
|
||||||
|
|
||||||
|
cog_name = cog.__cog_name__
|
||||||
|
existing = self.__cogs.get(cog_name)
|
||||||
|
|
||||||
|
if existing is not None:
|
||||||
|
if not override:
|
||||||
|
raise discord.ClientException(f'Cog named {cog_name!r} already loaded')
|
||||||
|
self.remove_cog(cog_name)
|
||||||
|
|
||||||
cog = cog._inject(self)
|
cog = cog._inject(self)
|
||||||
self.__cogs[cog.__cog_name__] = cog
|
self.__cogs[cog_name] = cog
|
||||||
|
|
||||||
def get_cog(self, name):
|
def get_cog(self, name):
|
||||||
"""Gets the cog instance requested.
|
"""Gets the cog instance requested.
|
||||||
@@ -845,7 +861,7 @@ class BotBase(GroupMixin):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
raise TypeError("command_prefix must be plain string, iterable of strings, or callable "
|
raise TypeError("command_prefix must be plain string, iterable of strings, or callable "
|
||||||
"returning either of these, not {}".format(ret.__class__.__name__))
|
f"returning either of these, not {ret.__class__.__name__}")
|
||||||
|
|
||||||
if not ret:
|
if not ret:
|
||||||
raise ValueError("Iterable command_prefix must contain at least one prefix")
|
raise ValueError("Iterable command_prefix must contain at least one prefix")
|
||||||
@@ -885,7 +901,7 @@ class BotBase(GroupMixin):
|
|||||||
view = StringView(message.content)
|
view = StringView(message.content)
|
||||||
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
||||||
|
|
||||||
if self._skip_check(message.author.id, self.user.id):
|
if message.author.id == self.user.id:
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
prefix = await self.get_prefix(message)
|
prefix = await self.get_prefix(message)
|
||||||
@@ -906,13 +922,13 @@ class BotBase(GroupMixin):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
if not isinstance(prefix, list):
|
if not isinstance(prefix, list):
|
||||||
raise TypeError("get_prefix must return either a string or a list of string, "
|
raise TypeError("get_prefix must return either a string or a list of string, "
|
||||||
"not {}".format(prefix.__class__.__name__))
|
f"not {prefix.__class__.__name__}")
|
||||||
|
|
||||||
# It's possible a bad command_prefix got us here.
|
# It's possible a bad command_prefix got us here.
|
||||||
for value in prefix:
|
for value in prefix:
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise TypeError("Iterable command_prefix or list returned from get_prefix must "
|
raise TypeError("Iterable command_prefix or list returned from get_prefix must "
|
||||||
"contain only strings, not {}".format(value.__class__.__name__))
|
f"contain only strings, not {value.__class__.__name__}")
|
||||||
|
|
||||||
# Getting here shouldn't happen
|
# Getting here shouldn't happen
|
||||||
raise
|
raise
|
||||||
@@ -1030,10 +1046,6 @@ class Bot(BotBase, discord.Client):
|
|||||||
you require group commands to be case insensitive as well.
|
you require group commands to be case insensitive as well.
|
||||||
description: :class:`str`
|
description: :class:`str`
|
||||||
The content prefixed into the default help message.
|
The content prefixed into the default help message.
|
||||||
self_bot: :class:`bool`
|
|
||||||
If ``True``, the bot will only listen to commands invoked by itself rather
|
|
||||||
than ignoring itself. If ``False`` (the default) then the bot will ignore
|
|
||||||
itself. This cannot be changed once initialised.
|
|
||||||
help_command: Optional[:class:`.HelpCommand`]
|
help_command: Optional[:class:`.HelpCommand`]
|
||||||
The help command implementation to use. This can be dynamically
|
The help command implementation to use. This can be dynamically
|
||||||
set at runtime. To remove the help command pass ``None``. For more
|
set at runtime. To remove the help command pass ``None``. For more
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class Cog(metaclass=CogMeta):
|
|||||||
|
|
||||||
@_cog_special_method
|
@_cog_special_method
|
||||||
def cog_check(self, ctx):
|
def cog_check(self, ctx):
|
||||||
"""A special method that registers as a :func:`commands.check`
|
"""A special method that registers as a :func:`~discord.ext.commands.check`
|
||||||
for every command and subcommand in this cog.
|
for every command and subcommand in this cog.
|
||||||
|
|
||||||
This function **can** be a coroutine and must take a sole parameter,
|
This function **can** be a coroutine and must take a sole parameter,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
import discord.abc
|
import discord.abc
|
||||||
import discord.utils
|
import discord.utils
|
||||||
|
import re
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Context',
|
'Context',
|
||||||
@@ -46,12 +47,17 @@ class Context(discord.abc.Messageable):
|
|||||||
The bot that contains the command being executed.
|
The bot that contains the command being executed.
|
||||||
args: :class:`list`
|
args: :class:`list`
|
||||||
The list of transformed arguments that were passed into the command.
|
The list of transformed arguments that were passed into the command.
|
||||||
If this is accessed during the :func:`on_command_error` event
|
If this is accessed during the :func:`.on_command_error` event
|
||||||
then this list could be incomplete.
|
then this list could be incomplete.
|
||||||
kwargs: :class:`dict`
|
kwargs: :class:`dict`
|
||||||
A dictionary of transformed arguments that were passed into the command.
|
A dictionary of transformed arguments that were passed into the command.
|
||||||
Similar to :attr:`args`\, if this is accessed in the
|
Similar to :attr:`args`\, if this is accessed in the
|
||||||
:func:`on_command_error` event then this dict could be incomplete.
|
:func:`.on_command_error` event then this dict could be incomplete.
|
||||||
|
current_parameter: Optional[:class:`inspect.Parameter`]
|
||||||
|
The parameter that is currently being inspected and converted.
|
||||||
|
This is only of use for within converters.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
prefix: :class:`str`
|
prefix: :class:`str`
|
||||||
The prefix that was used to invoke the command.
|
The prefix that was used to invoke the command.
|
||||||
command: :class:`Command`
|
command: :class:`Command`
|
||||||
@@ -93,6 +99,7 @@ class Context(discord.abc.Messageable):
|
|||||||
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
||||||
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
||||||
self.command_failed = attrs.pop('command_failed', False)
|
self.command_failed = attrs.pop('command_failed', False)
|
||||||
|
self.current_parameter = attrs.pop('current_parameter', None)
|
||||||
self._state = self.message._state
|
self._state = self.message._state
|
||||||
|
|
||||||
async def invoke(self, command, /, *args, **kwargs):
|
async def invoke(self, command, /, *args, **kwargs):
|
||||||
@@ -136,7 +143,7 @@ class Context(discord.abc.Messageable):
|
|||||||
ret = await command.callback(*arguments, **kwargs)
|
ret = await command.callback(*arguments, **kwargs)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def reinvoke(self, *, call_hooks=False, restart=True):
|
async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Calls the command again.
|
Calls the command again.
|
||||||
@@ -206,6 +213,20 @@ class Context(discord.abc.Messageable):
|
|||||||
async def _get_channel(self):
|
async def _get_channel(self):
|
||||||
return self.channel
|
return self.channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clean_prefix(self):
|
||||||
|
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
user = self.guild.me if self.guild else self.bot.user
|
||||||
|
# this breaks if the prefix mention is not the bot itself but I
|
||||||
|
# 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.prefix)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cog(self):
|
def cog(self):
|
||||||
"""Optional[: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."""
|
||||||
|
|||||||
@@ -26,7 +26,22 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Generic,
|
||||||
|
Iterable,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
List,
|
||||||
|
Protocol,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
runtime_checkable,
|
||||||
|
)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from .errors import *
|
from .errors import *
|
||||||
@@ -37,6 +52,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Converter',
|
'Converter',
|
||||||
|
'ObjectConverter',
|
||||||
'MemberConverter',
|
'MemberConverter',
|
||||||
'UserConverter',
|
'UserConverter',
|
||||||
'MessageConverter',
|
'MessageConverter',
|
||||||
@@ -55,8 +71,10 @@ __all__ = (
|
|||||||
'CategoryChannelConverter',
|
'CategoryChannelConverter',
|
||||||
'IDConverter',
|
'IDConverter',
|
||||||
'StoreChannelConverter',
|
'StoreChannelConverter',
|
||||||
|
'GuildChannelConverter',
|
||||||
'clean_content',
|
'clean_content',
|
||||||
'Greedy',
|
'Greedy',
|
||||||
|
'run_converters',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -70,11 +88,13 @@ def _get_from_guilds(bot, getter, argument):
|
|||||||
|
|
||||||
|
|
||||||
_utils_get = discord.utils.get
|
_utils_get = discord.utils.get
|
||||||
T = TypeVar('T', covariant=True)
|
T = TypeVar('T')
|
||||||
|
T_co = TypeVar('T_co', covariant=True)
|
||||||
|
CT = TypeVar('CT', bound=discord.abc.GuildChannel)
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Converter(Protocol[T]):
|
class Converter(Protocol[T_co]):
|
||||||
"""The base class of custom converters that require the :class:`.Context`
|
"""The base class of custom converters that require the :class:`.Context`
|
||||||
to be passed to be useful.
|
to be passed to be useful.
|
||||||
|
|
||||||
@@ -85,7 +105,7 @@ class Converter(Protocol[T]):
|
|||||||
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
|
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> T:
|
async def convert(self, ctx: Context, argument: str) -> T_co:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
The method to override to do conversion logic.
|
The method to override to do conversion logic.
|
||||||
@@ -110,13 +130,39 @@ class Converter(Protocol[T]):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError('Derived classes need to implement this.')
|
raise NotImplementedError('Derived classes need to implement this.')
|
||||||
|
|
||||||
class IDConverter(Converter[T]):
|
|
||||||
def __init__(self):
|
|
||||||
self._id_regex = re.compile(r'([0-9]{15,20})$')
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def _get_id_match(self, argument):
|
_ID_REGEX = re.compile(r'([0-9]{15,20})$')
|
||||||
return self._id_regex.match(argument)
|
|
||||||
|
|
||||||
|
class IDConverter(Converter[T_co]):
|
||||||
|
@staticmethod
|
||||||
|
def _get_id_match(argument):
|
||||||
|
return _ID_REGEX.match(argument)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectConverter(IDConverter[discord.Object]):
|
||||||
|
"""Converts to a :class:`~discord.Object`.
|
||||||
|
|
||||||
|
The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`).
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
The lookup strategy is as follows (in order):
|
||||||
|
|
||||||
|
1. Lookup by ID.
|
||||||
|
2. Lookup by member, role, or channel mention.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def convert(self, ctx: Context, argument: str) -> discord.Object:
|
||||||
|
match = self._get_id_match(argument) or re.match(r'<(?:@(?:!|&)?|#)([0-9]{15,20})>$', argument)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
raise ObjectNotFound(argument)
|
||||||
|
|
||||||
|
result = int(match.group(1))
|
||||||
|
|
||||||
|
return discord.Object(id=result)
|
||||||
|
|
||||||
|
|
||||||
class MemberConverter(IDConverter[discord.Member]):
|
class MemberConverter(IDConverter[discord.Member]):
|
||||||
"""Converts to a :class:`~discord.Member`.
|
"""Converts to a :class:`~discord.Member`.
|
||||||
@@ -173,7 +219,7 @@ class MemberConverter(IDConverter[discord.Member]):
|
|||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Member:
|
async def convert(self, ctx: Context, argument: str) -> discord.Member:
|
||||||
bot = ctx.bot
|
bot = ctx.bot
|
||||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]{15,20})>$', argument)
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
result = None
|
result = None
|
||||||
user_id = None
|
user_id = None
|
||||||
@@ -204,6 +250,7 @@ class MemberConverter(IDConverter[discord.Member]):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class UserConverter(IDConverter[discord.User]):
|
class UserConverter(IDConverter[discord.User]):
|
||||||
"""Converts to a :class:`~discord.User`.
|
"""Converts to a :class:`~discord.User`.
|
||||||
|
|
||||||
@@ -223,8 +270,9 @@ class UserConverter(IDConverter[discord.User]):
|
|||||||
This converter now lazily fetches users from the HTTP APIs if an ID is passed
|
This converter now lazily fetches users from the HTTP APIs if an ID is passed
|
||||||
and it's not available in cache.
|
and it's not available in cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.User:
|
async def convert(self, ctx: Context, argument: str) -> discord.User:
|
||||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]{15,20})>$', argument)
|
||||||
result = None
|
result = None
|
||||||
state = ctx._state
|
state = ctx._state
|
||||||
|
|
||||||
@@ -263,6 +311,7 @@ class UserConverter(IDConverter[discord.User]):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class PartialMessageConverter(Converter[discord.PartialMessage]):
|
class PartialMessageConverter(Converter[discord.PartialMessage]):
|
||||||
"""Converts to a :class:`discord.PartialMessage`.
|
"""Converts to a :class:`discord.PartialMessage`.
|
||||||
|
|
||||||
@@ -274,6 +323,7 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
|
|||||||
2. By message ID (The message is assumed to be in the context channel.)
|
2. By message ID (The message is assumed to be in the context channel.)
|
||||||
3. By message URL
|
3. By message URL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_id_matches(argument):
|
def _get_id_matches(argument):
|
||||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
|
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
|
||||||
@@ -285,8 +335,8 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
|
|||||||
match = id_regex.match(argument) or link_regex.match(argument)
|
match = id_regex.match(argument) or link_regex.match(argument)
|
||||||
if not match:
|
if not match:
|
||||||
raise MessageNotFound(argument)
|
raise MessageNotFound(argument)
|
||||||
channel_id = match.group("channel_id")
|
channel_id = match.group('channel_id')
|
||||||
return int(match.group("message_id")), int(channel_id) if channel_id else None
|
return int(match.group('message_id')), int(channel_id) if channel_id else None
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
|
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
|
||||||
message_id, channel_id = self._get_id_matches(argument)
|
message_id, channel_id = self._get_id_matches(argument)
|
||||||
@@ -295,6 +345,7 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
|
|||||||
raise ChannelNotFound(channel_id)
|
raise ChannelNotFound(channel_id)
|
||||||
return discord.PartialMessage(channel=channel, id=message_id)
|
return discord.PartialMessage(channel=channel, id=message_id)
|
||||||
|
|
||||||
|
|
||||||
class MessageConverter(IDConverter[discord.Message]):
|
class MessageConverter(IDConverter[discord.Message]):
|
||||||
"""Converts to a :class:`discord.Message`.
|
"""Converts to a :class:`discord.Message`.
|
||||||
|
|
||||||
@@ -309,6 +360,7 @@ class MessageConverter(IDConverter[discord.Message]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Message:
|
async def convert(self, ctx: Context, argument: str) -> discord.Message:
|
||||||
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
|
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
|
||||||
message = ctx.bot._connection._get_message(message_id)
|
message = ctx.bot._connection._get_message(message_id)
|
||||||
@@ -324,6 +376,56 @@ class MessageConverter(IDConverter[discord.Message]):
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
raise ChannelNotReadable(channel)
|
raise ChannelNotReadable(channel)
|
||||||
|
|
||||||
|
|
||||||
|
class GuildChannelConverter(IDConverter[discord.abc.GuildChannel]):
|
||||||
|
"""Converts to a :class:`~discord.abc.GuildChannel`.
|
||||||
|
|
||||||
|
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:: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def convert(self, ctx: Context, argument: str) -> discord.abc.GuildChannel:
|
||||||
|
return self._resolve_channel(ctx, argument, ctx.guild.channels, discord.abc.GuildChannel)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_channel(ctx: Context, argument: str, iterable: Iterable[CT], type: Type[CT]) -> CT:
|
||||||
|
bot = ctx.bot
|
||||||
|
|
||||||
|
match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument)
|
||||||
|
result = None
|
||||||
|
guild = ctx.guild
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
# not a mention
|
||||||
|
if guild:
|
||||||
|
result: Optional[CT] = discord.utils.get(iterable, name=argument)
|
||||||
|
else:
|
||||||
|
|
||||||
|
def check(c):
|
||||||
|
return isinstance(c, type) 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, type):
|
||||||
|
raise ChannelNotFound(argument)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class TextChannelConverter(IDConverter[discord.TextChannel]):
|
class TextChannelConverter(IDConverter[discord.TextChannel]):
|
||||||
"""Converts to a :class:`~discord.TextChannel`.
|
"""Converts to a :class:`~discord.TextChannel`.
|
||||||
|
|
||||||
@@ -339,32 +441,10 @@ class TextChannelConverter(IDConverter[discord.TextChannel]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
|
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
|
||||||
bot = ctx.bot
|
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.text_channels, discord.TextChannel)
|
||||||
|
|
||||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
|
||||||
result = None
|
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
if match is None:
|
|
||||||
# not a mention
|
|
||||||
if guild:
|
|
||||||
result = discord.utils.get(guild.text_channels, name=argument)
|
|
||||||
else:
|
|
||||||
def check(c):
|
|
||||||
return isinstance(c, discord.TextChannel) and c.name == argument
|
|
||||||
result = discord.utils.find(check, bot.get_all_channels())
|
|
||||||
else:
|
|
||||||
channel_id = int(match.group(1))
|
|
||||||
if guild:
|
|
||||||
result = guild.get_channel(channel_id)
|
|
||||||
else:
|
|
||||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
|
||||||
|
|
||||||
if not isinstance(result, discord.TextChannel):
|
|
||||||
raise ChannelNotFound(argument)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
|
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
|
||||||
"""Converts to a :class:`~discord.VoiceChannel`.
|
"""Converts to a :class:`~discord.VoiceChannel`.
|
||||||
@@ -381,31 +461,10 @@ class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
|
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
|
||||||
bot = ctx.bot
|
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.voice_channels, discord.VoiceChannel)
|
||||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
|
||||||
result = None
|
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
if match is None:
|
|
||||||
# not a mention
|
|
||||||
if guild:
|
|
||||||
result = discord.utils.get(guild.voice_channels, name=argument)
|
|
||||||
else:
|
|
||||||
def check(c):
|
|
||||||
return isinstance(c, discord.VoiceChannel) and c.name == argument
|
|
||||||
result = discord.utils.find(check, bot.get_all_channels())
|
|
||||||
else:
|
|
||||||
channel_id = int(match.group(1))
|
|
||||||
if guild:
|
|
||||||
result = guild.get_channel(channel_id)
|
|
||||||
else:
|
|
||||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
|
||||||
|
|
||||||
if not isinstance(result, discord.VoiceChannel):
|
|
||||||
raise ChannelNotFound(argument)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
class StageChannelConverter(IDConverter[discord.StageChannel]):
|
class StageChannelConverter(IDConverter[discord.StageChannel]):
|
||||||
"""Converts to a :class:`~discord.StageChannel`.
|
"""Converts to a :class:`~discord.StageChannel`.
|
||||||
@@ -421,31 +480,10 @@ class StageChannelConverter(IDConverter[discord.StageChannel]):
|
|||||||
2. Lookup by mention.
|
2. Lookup by mention.
|
||||||
3. Lookup by name
|
3. Lookup by name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
|
async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
|
||||||
bot = ctx.bot
|
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.stage_channels, discord.StageChannel)
|
||||||
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]):
|
class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
|
||||||
"""Converts to a :class:`~discord.CategoryChannel`.
|
"""Converts to a :class:`~discord.CategoryChannel`.
|
||||||
@@ -462,32 +500,10 @@ class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
|
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
|
||||||
bot = ctx.bot
|
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.categories, discord.CategoryChannel)
|
||||||
|
|
||||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
|
||||||
result = None
|
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
if match is None:
|
|
||||||
# not a mention
|
|
||||||
if guild:
|
|
||||||
result = discord.utils.get(guild.categories, name=argument)
|
|
||||||
else:
|
|
||||||
def check(c):
|
|
||||||
return isinstance(c, discord.CategoryChannel) and c.name == argument
|
|
||||||
result = discord.utils.find(check, bot.get_all_channels())
|
|
||||||
else:
|
|
||||||
channel_id = int(match.group(1))
|
|
||||||
if guild:
|
|
||||||
result = guild.get_channel(channel_id)
|
|
||||||
else:
|
|
||||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
|
||||||
|
|
||||||
if not isinstance(result, discord.CategoryChannel):
|
|
||||||
raise ChannelNotFound(argument)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
|
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
|
||||||
"""Converts to a :class:`~discord.StoreChannel`.
|
"""Converts to a :class:`~discord.StoreChannel`.
|
||||||
@@ -505,30 +521,8 @@ class StoreChannelConverter(IDConverter[discord.StoreChannel]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
|
async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
|
||||||
bot = ctx.bot
|
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.channels, discord.StoreChannel)
|
||||||
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]):
|
class ColourConverter(Converter[discord.Colour]):
|
||||||
"""Converts to a :class:`~discord.Colour`.
|
"""Converts to a :class:`~discord.Colour`.
|
||||||
@@ -542,7 +536,7 @@ class ColourConverter(Converter[discord.Colour]):
|
|||||||
- ``#<hex>``
|
- ``#<hex>``
|
||||||
- ``0x#<hex>``
|
- ``0x#<hex>``
|
||||||
- ``rgb(<number>, <number>, <number>)``
|
- ``rgb(<number>, <number>, <number>)``
|
||||||
- Any of the ``classmethod`` in :class:`Colour`
|
- Any of the ``classmethod`` in :class:`~discord.Colour`
|
||||||
|
|
||||||
- The ``_`` in the name can be optionally replaced with spaces.
|
- The ``_`` in the name can be optionally replaced with spaces.
|
||||||
|
|
||||||
@@ -612,13 +606,15 @@ class ColourConverter(Converter[discord.Colour]):
|
|||||||
raise BadColourArgument(arg)
|
raise BadColourArgument(arg)
|
||||||
return method()
|
return method()
|
||||||
|
|
||||||
|
|
||||||
ColorConverter = ColourConverter
|
ColorConverter = ColourConverter
|
||||||
|
|
||||||
|
|
||||||
class RoleConverter(IDConverter[discord.Role]):
|
class RoleConverter(IDConverter[discord.Role]):
|
||||||
"""Converts to a :class:`~discord.Role`.
|
"""Converts to a :class:`~discord.Role`.
|
||||||
|
|
||||||
All lookups are via the local guild. If in a DM context, then the lookup
|
All lookups are via the local guild. If in a DM context, the converter raises
|
||||||
is done by the global cache.
|
:exc:`.NoPrivateMessage` exception.
|
||||||
|
|
||||||
The lookup strategy is as follows (in order):
|
The lookup strategy is as follows (in order):
|
||||||
|
|
||||||
@@ -629,12 +625,13 @@ class RoleConverter(IDConverter[discord.Role]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Role:
|
async def convert(self, ctx: Context, argument: str) -> discord.Role:
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if not guild:
|
if not guild:
|
||||||
raise NoPrivateMessage()
|
raise NoPrivateMessage()
|
||||||
|
|
||||||
match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument)
|
match = self._get_id_match(argument) or re.match(r'<@&([0-9]{15,20})>$', argument)
|
||||||
if match:
|
if match:
|
||||||
result = guild.get_role(int(match.group(1)))
|
result = guild.get_role(int(match.group(1)))
|
||||||
else:
|
else:
|
||||||
@@ -644,11 +641,14 @@ class RoleConverter(IDConverter[discord.Role]):
|
|||||||
raise RoleNotFound(argument)
|
raise RoleNotFound(argument)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class GameConverter(Converter[discord.Game]):
|
class GameConverter(Converter[discord.Game]):
|
||||||
"""Converts to :class:`~discord.Game`."""
|
"""Converts to :class:`~discord.Game`."""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Game:
|
async def convert(self, ctx: Context, argument: str) -> discord.Game:
|
||||||
return discord.Game(name=argument)
|
return discord.Game(name=argument)
|
||||||
|
|
||||||
|
|
||||||
class InviteConverter(Converter[discord.Invite]):
|
class InviteConverter(Converter[discord.Invite]):
|
||||||
"""Converts to a :class:`~discord.Invite`.
|
"""Converts to a :class:`~discord.Invite`.
|
||||||
|
|
||||||
@@ -657,6 +657,7 @@ class InviteConverter(Converter[discord.Invite]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
|
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
|
||||||
try:
|
try:
|
||||||
invite = await ctx.bot.fetch_invite(argument)
|
invite = await ctx.bot.fetch_invite(argument)
|
||||||
@@ -664,6 +665,7 @@ class InviteConverter(Converter[discord.Invite]):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise BadInviteArgument() from exc
|
raise BadInviteArgument() from exc
|
||||||
|
|
||||||
|
|
||||||
class GuildConverter(IDConverter[discord.Guild]):
|
class GuildConverter(IDConverter[discord.Guild]):
|
||||||
"""Converts to a :class:`~discord.Guild`.
|
"""Converts to a :class:`~discord.Guild`.
|
||||||
|
|
||||||
@@ -690,6 +692,7 @@ class GuildConverter(IDConverter[discord.Guild]):
|
|||||||
raise GuildNotFound(argument)
|
raise GuildNotFound(argument)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class EmojiConverter(IDConverter[discord.Emoji]):
|
class EmojiConverter(IDConverter[discord.Emoji]):
|
||||||
"""Converts to a :class:`~discord.Emoji`.
|
"""Converts to a :class:`~discord.Emoji`.
|
||||||
|
|
||||||
@@ -705,8 +708,9 @@ class EmojiConverter(IDConverter[discord.Emoji]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
|
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
|
||||||
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
|
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]{1,32}:([0-9]{15,20})>$', argument)
|
||||||
result = None
|
result = None
|
||||||
bot = ctx.bot
|
bot = ctx.bot
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -733,6 +737,7 @@ class EmojiConverter(IDConverter[discord.Emoji]):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
|
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
|
||||||
"""Converts to a :class:`~discord.PartialEmoji`.
|
"""Converts to a :class:`~discord.PartialEmoji`.
|
||||||
|
|
||||||
@@ -741,19 +746,22 @@ class PartialEmojiConverter(Converter[discord.PartialEmoji]):
|
|||||||
.. versionchanged:: 1.5
|
.. versionchanged:: 1.5
|
||||||
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
|
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
|
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
|
||||||
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
|
match = re.match(r'<(a?):([a-zA-Z0-9\_]{1,32}):([0-9]{15,20})>$', argument)
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
emoji_animated = bool(match.group(1))
|
emoji_animated = bool(match.group(1))
|
||||||
emoji_name = match.group(2)
|
emoji_name = match.group(2)
|
||||||
emoji_id = int(match.group(3))
|
emoji_id = int(match.group(3))
|
||||||
|
|
||||||
return discord.PartialEmoji.with_state(ctx.bot._connection, animated=emoji_animated, name=emoji_name,
|
return discord.PartialEmoji.with_state(
|
||||||
id=emoji_id)
|
ctx.bot._connection, animated=emoji_animated, name=emoji_name, id=emoji_id
|
||||||
|
)
|
||||||
|
|
||||||
raise PartialEmojiConversionFailure(argument)
|
raise PartialEmojiConversionFailure(argument)
|
||||||
|
|
||||||
|
|
||||||
class clean_content(Converter[str]):
|
class clean_content(Converter[str]):
|
||||||
"""Converts the argument to mention scrubbed version of
|
"""Converts the argument to mention scrubbed version of
|
||||||
said content.
|
said content.
|
||||||
@@ -773,7 +781,8 @@ class clean_content(Converter[str]):
|
|||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
"""
|
"""
|
||||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
|
|
||||||
|
def __init__(self, *, fix_channel_mentions: bool = False, use_nicknames: bool = True, escape_markdown: bool = False, remove_markdown: bool = False) -> None:
|
||||||
self.fix_channel_mentions = fix_channel_mentions
|
self.fix_channel_mentions = fix_channel_mentions
|
||||||
self.use_nicknames = use_nicknames
|
self.use_nicknames = use_nicknames
|
||||||
self.escape_markdown = escape_markdown
|
self.escape_markdown = escape_markdown
|
||||||
@@ -784,6 +793,7 @@ class clean_content(Converter[str]):
|
|||||||
transformations = {}
|
transformations = {}
|
||||||
|
|
||||||
if self.fix_channel_mentions and ctx.guild:
|
if self.fix_channel_mentions and ctx.guild:
|
||||||
|
|
||||||
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
||||||
ch = _get(id)
|
ch = _get(id)
|
||||||
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
|
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
|
||||||
@@ -791,15 +801,18 @@ class clean_content(Converter[str]):
|
|||||||
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
||||||
|
|
||||||
if self.use_nicknames and ctx.guild:
|
if self.use_nicknames and ctx.guild:
|
||||||
|
|
||||||
def resolve_member(id, *, _get=ctx.guild.get_member):
|
def resolve_member(id, *, _get=ctx.guild.get_member):
|
||||||
m = _get(id)
|
m = _get(id)
|
||||||
return '@' + m.display_name if m else '@deleted-user'
|
return '@' + m.display_name if m else '@deleted-user'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def resolve_member(id, *, _get=ctx.bot.get_user):
|
def resolve_member(id, *, _get=ctx.bot.get_user):
|
||||||
m = _get(id)
|
m = _get(id)
|
||||||
return '@' + m.name if m else '@deleted-user'
|
return '@' + m.name if m else '@deleted-user'
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
transformations.update(
|
transformations.update(
|
||||||
(f'<@{member_id}>', resolve_member(member_id))
|
(f'<@{member_id}>', resolve_member(member_id))
|
||||||
for member_id in message.raw_mentions
|
for member_id in message.raw_mentions
|
||||||
@@ -809,16 +822,20 @@ class clean_content(Converter[str]):
|
|||||||
(f'<@!{member_id}>', resolve_member(member_id))
|
(f'<@!{member_id}>', resolve_member(member_id))
|
||||||
for member_id in message.raw_mentions
|
for member_id in message.raw_mentions
|
||||||
)
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
|
|
||||||
def resolve_role(_id, *, _find=ctx.guild.get_role):
|
def resolve_role(_id, *, _find=ctx.guild.get_role):
|
||||||
r = _find(_id)
|
r = _find(_id)
|
||||||
return '@' + r.name if r else '@deleted-role'
|
return '@' + r.name if r else '@deleted-role'
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
transformations.update(
|
transformations.update(
|
||||||
(f'<@&{role_id}>', resolve_role(role_id))
|
(f'<@&{role_id}>', resolve_role(role_id))
|
||||||
for role_id in message.raw_role_mentions
|
for role_id in message.raw_role_mentions
|
||||||
)
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
def repl(obj):
|
def repl(obj):
|
||||||
return transformations.get(obj.group(0), '')
|
return transformations.get(obj.group(0), '')
|
||||||
@@ -834,28 +851,230 @@ class clean_content(Converter[str]):
|
|||||||
# Completely ensure no mentions escape:
|
# Completely ensure no mentions escape:
|
||||||
return discord.utils.escape_mentions(result)
|
return discord.utils.escape_mentions(result)
|
||||||
|
|
||||||
class _Greedy:
|
|
||||||
|
class Greedy(List[T]):
|
||||||
|
r"""A special converter that greedily consumes arguments until it can't.
|
||||||
|
As a consequence of this behaviour, most input errors are silently discarded,
|
||||||
|
since it is used as an indicator of when to stop parsing.
|
||||||
|
|
||||||
|
When a parser error is met the greedy converter stops converting, undoes the
|
||||||
|
internal string parsing routine, and continues parsing regularly.
|
||||||
|
|
||||||
|
For example, in the following code:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def test(ctx, numbers: Greedy[int], reason: str):
|
||||||
|
await ctx.send("numbers: {}, reason: {}".format(numbers, reason))
|
||||||
|
|
||||||
|
An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with
|
||||||
|
``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\.
|
||||||
|
|
||||||
|
For more information, check :ref:`ext_commands_special_converters`.
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ('converter',)
|
__slots__ = ('converter',)
|
||||||
|
|
||||||
def __init__(self, *, converter=None):
|
def __init__(self, *, converter: T):
|
||||||
self.converter = converter
|
self.converter = converter
|
||||||
|
|
||||||
def __getitem__(self, params):
|
def __repr__(self):
|
||||||
|
converter = getattr(self.converter, '__name__', repr(self.converter))
|
||||||
|
return f'Greedy[{converter}]'
|
||||||
|
|
||||||
|
def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]:
|
||||||
if not isinstance(params, tuple):
|
if not isinstance(params, tuple):
|
||||||
params = (params,)
|
params = (params,)
|
||||||
if len(params) != 1:
|
if len(params) != 1:
|
||||||
raise TypeError('Greedy[...] only takes a single argument')
|
raise TypeError('Greedy[...] only takes a single argument')
|
||||||
converter = params[0]
|
converter = params[0]
|
||||||
|
|
||||||
if not (callable(converter) or isinstance(converter, Converter) or hasattr(converter, '__origin__')):
|
origin = getattr(converter, '__origin__', None)
|
||||||
|
args = getattr(converter, '__args__', ())
|
||||||
|
|
||||||
|
if not (callable(converter) or isinstance(converter, Converter) or origin is not None):
|
||||||
raise TypeError('Greedy[...] expects a type or a Converter instance.')
|
raise TypeError('Greedy[...] expects a type or a Converter instance.')
|
||||||
|
|
||||||
if converter is str or converter is type(None) or converter is _Greedy:
|
if converter in (str, type(None)) or origin is Greedy:
|
||||||
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
|
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
|
||||||
|
|
||||||
if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
|
if origin is Union and type(None) in args:
|
||||||
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
||||||
|
|
||||||
return self.__class__(converter=converter)
|
return cls(converter=converter)
|
||||||
|
|
||||||
Greedy = _Greedy()
|
|
||||||
|
def _convert_to_bool(argument: str) -> bool:
|
||||||
|
lowered = argument.lower()
|
||||||
|
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
||||||
|
return True
|
||||||
|
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise BadBoolArgument(lowered)
|
||||||
|
|
||||||
|
|
||||||
|
def get_converter(param: inspect.Parameter) -> Any:
|
||||||
|
converter = param.annotation
|
||||||
|
if converter is param.empty:
|
||||||
|
if param.default is not param.empty:
|
||||||
|
converter = str if param.default is None else type(param.default)
|
||||||
|
else:
|
||||||
|
converter = str
|
||||||
|
return converter
|
||||||
|
|
||||||
|
|
||||||
|
_GenericAlias = type(List[T])
|
||||||
|
|
||||||
|
|
||||||
|
def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool:
|
||||||
|
return isinstance(tp, type) and issubclass(tp, Generic) or isinstance(tp, _GenericAlias) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
CONVERTER_MAPPING: Dict[Type[Any], Any] = {
|
||||||
|
discord.Object: ObjectConverter,
|
||||||
|
discord.Member: MemberConverter,
|
||||||
|
discord.User: UserConverter,
|
||||||
|
discord.Message: MessageConverter,
|
||||||
|
discord.PartialMessage: PartialMessageConverter,
|
||||||
|
discord.TextChannel: TextChannelConverter,
|
||||||
|
discord.Invite: InviteConverter,
|
||||||
|
discord.Guild: GuildConverter,
|
||||||
|
discord.Role: RoleConverter,
|
||||||
|
discord.Game: GameConverter,
|
||||||
|
discord.Colour: ColourConverter,
|
||||||
|
discord.VoiceChannel: VoiceChannelConverter,
|
||||||
|
discord.StageChannel: StageChannelConverter,
|
||||||
|
discord.Emoji: EmojiConverter,
|
||||||
|
discord.PartialEmoji: PartialEmojiConverter,
|
||||||
|
discord.CategoryChannel: CategoryChannelConverter,
|
||||||
|
discord.StoreChannel: StoreChannelConverter,
|
||||||
|
discord.abc.GuildChannel: GuildChannelConverter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _actual_conversion(ctx: Context, converter, argument: str, param: inspect.Parameter):
|
||||||
|
if converter is bool:
|
||||||
|
return _convert_to_bool(argument)
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = converter.__module__
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
|
||||||
|
converter = CONVERTER_MAPPING.get(converter, converter)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if inspect.isclass(converter) and issubclass(converter, Converter):
|
||||||
|
if inspect.ismethod(converter.convert):
|
||||||
|
return await converter.convert(ctx, argument)
|
||||||
|
else:
|
||||||
|
return await converter().convert(ctx, argument)
|
||||||
|
elif isinstance(converter, Converter):
|
||||||
|
return await converter.convert(ctx, argument)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise ConversionError(converter, exc) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
return converter(argument)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
name = converter.__name__
|
||||||
|
except AttributeError:
|
||||||
|
name = converter.__class__.__name__
|
||||||
|
|
||||||
|
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def run_converters(ctx: Context, converter, argument: str, param: inspect.Parameter):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Runs converters for a given converter, argument, and parameter.
|
||||||
|
|
||||||
|
This function does the same work that the library does under the hood.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
ctx: :class:`Context`
|
||||||
|
The invocation context to run the converters under.
|
||||||
|
converter: Any
|
||||||
|
The converter to run, this corresponds to the annotation in the function.
|
||||||
|
argument: :class:`str`
|
||||||
|
The argument to convert to.
|
||||||
|
param: :class:`inspect.Parameter`
|
||||||
|
The parameter being converted. This is mainly for error reporting.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
CommandError
|
||||||
|
The converter failed to convert.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Any
|
||||||
|
The resulting conversion.
|
||||||
|
"""
|
||||||
|
origin = getattr(converter, '__origin__', None)
|
||||||
|
|
||||||
|
if origin is Union:
|
||||||
|
errors = []
|
||||||
|
_NoneType = type(None)
|
||||||
|
union_args = converter.__args__
|
||||||
|
for conv in union_args:
|
||||||
|
# if we got to this part in the code, then the previous conversions have failed
|
||||||
|
# so we should just undo the view, return the default, and allow parsing to continue
|
||||||
|
# with the other parameters
|
||||||
|
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
|
||||||
|
ctx.view.undo()
|
||||||
|
return None if param.default is param.empty else param.default
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = await run_converters(ctx, conv, argument, param)
|
||||||
|
except CommandError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# if we're here, then we failed all the converters
|
||||||
|
raise BadUnionArgument(param, union_args, errors)
|
||||||
|
|
||||||
|
if origin is Literal:
|
||||||
|
errors = []
|
||||||
|
conversions = {}
|
||||||
|
literal_args = converter.__args__
|
||||||
|
for literal in literal_args:
|
||||||
|
literal_type = type(literal)
|
||||||
|
try:
|
||||||
|
value = conversions[literal_type]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
value = await _actual_conversion(ctx, literal_type, argument, param)
|
||||||
|
except CommandError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
conversions[literal_type] = object()
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
conversions[literal_type] = value
|
||||||
|
|
||||||
|
if value == literal:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# if we're here, then we failed to match all the literals
|
||||||
|
raise BadLiteralArgument(param, literal_args, errors)
|
||||||
|
|
||||||
|
# This must be the last if-clause in the chain of origin checking
|
||||||
|
# Nearly every type is a generic type within the typing library
|
||||||
|
# So care must be taken to make sure a more specialised origin handle
|
||||||
|
# isn't overwritten by the widest if clause
|
||||||
|
if origin is not None and is_generic_type(converter):
|
||||||
|
converter = origin
|
||||||
|
|
||||||
|
return await _actual_conversion(ctx, converter, argument, param)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'BucketType',
|
'BucketType',
|
||||||
'Cooldown',
|
'Cooldown',
|
||||||
'CooldownMapping',
|
'CooldownMapping',
|
||||||
|
'DynamicCooldownMapping',
|
||||||
'MaxConcurrency',
|
'MaxConcurrency',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,19 +70,15 @@ class BucketType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Cooldown:
|
class Cooldown:
|
||||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
__slots__ = ('rate', 'per', '_window', '_tokens', '_last')
|
||||||
|
|
||||||
def __init__(self, rate, per, type):
|
def __init__(self, rate, per):
|
||||||
self.rate = int(rate)
|
self.rate = int(rate)
|
||||||
self.per = float(per)
|
self.per = float(per)
|
||||||
self.type = type
|
|
||||||
self._window = 0.0
|
self._window = 0.0
|
||||||
self._tokens = self.rate
|
self._tokens = self.rate
|
||||||
self._last = 0.0
|
self._last = 0.0
|
||||||
|
|
||||||
if not callable(self.type):
|
|
||||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
|
||||||
|
|
||||||
def get_tokens(self, current=None):
|
def get_tokens(self, current=None):
|
||||||
if not current:
|
if not current:
|
||||||
current = time.time()
|
current = time.time()
|
||||||
@@ -128,18 +125,22 @@ class Cooldown:
|
|||||||
self._last = 0.0
|
self._last = 0.0
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
return Cooldown(self.rate, self.per, self.type)
|
return Cooldown(self.rate, self.per)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.format(self)
|
return f'<Cooldown rate: {self.rate} per: {self.per} window: {self._window} tokens: {self._tokens}>'
|
||||||
|
|
||||||
class CooldownMapping:
|
class CooldownMapping:
|
||||||
def __init__(self, original):
|
def __init__(self, original, type):
|
||||||
|
if not callable(type):
|
||||||
|
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||||
|
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
self._cooldown = original
|
self._cooldown = original
|
||||||
|
self._type = type
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
ret = CooldownMapping(self._cooldown)
|
ret = CooldownMapping(self._cooldown, self._type)
|
||||||
ret._cache = self._cache.copy()
|
ret._cache = self._cache.copy()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -149,10 +150,10 @@ class CooldownMapping:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_cooldown(cls, rate, per, type):
|
def from_cooldown(cls, rate, per, type):
|
||||||
return cls(Cooldown(rate, per, type))
|
return cls(Cooldown(rate, per), type)
|
||||||
|
|
||||||
def _bucket_key(self, msg):
|
def _bucket_key(self, msg):
|
||||||
return self._cooldown.type(msg)
|
return self._type(msg)
|
||||||
|
|
||||||
def _verify_cache_integrity(self, current=None):
|
def _verify_cache_integrity(self, current=None):
|
||||||
# we want to delete all cache objects that haven't been used
|
# we want to delete all cache objects that haven't been used
|
||||||
@@ -163,15 +164,19 @@ class CooldownMapping:
|
|||||||
for k in dead_keys:
|
for k in dead_keys:
|
||||||
del self._cache[k]
|
del self._cache[k]
|
||||||
|
|
||||||
|
def create_bucket(self, message):
|
||||||
|
return self._cooldown.copy()
|
||||||
|
|
||||||
def get_bucket(self, message, current=None):
|
def get_bucket(self, message, current=None):
|
||||||
if self._cooldown.type is BucketType.default:
|
if self._type is BucketType.default:
|
||||||
return self._cooldown
|
return self._cooldown
|
||||||
|
|
||||||
self._verify_cache_integrity(current)
|
self._verify_cache_integrity(current)
|
||||||
key = self._bucket_key(message)
|
key = self._bucket_key(message)
|
||||||
if key not in self._cache:
|
if key not in self._cache:
|
||||||
bucket = self._cooldown.copy()
|
bucket = self.create_bucket(message)
|
||||||
self._cache[key] = bucket
|
if bucket is not None:
|
||||||
|
self._cache[key] = bucket
|
||||||
else:
|
else:
|
||||||
bucket = self._cache[key]
|
bucket = self._cache[key]
|
||||||
|
|
||||||
@@ -181,6 +186,24 @@ class CooldownMapping:
|
|||||||
bucket = self.get_bucket(message, current)
|
bucket = self.get_bucket(message, current)
|
||||||
return bucket.update_rate_limit(current)
|
return bucket.update_rate_limit(current)
|
||||||
|
|
||||||
|
class DynamicCooldownMapping(CooldownMapping):
|
||||||
|
|
||||||
|
def __init__(self, factory, type):
|
||||||
|
super().__init__(None, type)
|
||||||
|
self._factory = factory
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
ret = DynamicCooldownMapping(self._factory, self._type)
|
||||||
|
ret._cache = self._cache.copy()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_bucket(self, message):
|
||||||
|
return self._factory(message)
|
||||||
|
|
||||||
class _Semaphore:
|
class _Semaphore:
|
||||||
"""This class is a version of a semaphore.
|
"""This class is a version of a semaphore.
|
||||||
|
|
||||||
@@ -202,7 +225,7 @@ class _Semaphore:
|
|||||||
self._waiters = deque()
|
self._waiters = deque()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<_Semaphore value={0.value} waiters={1}>'.format(self, len(self._waiters))
|
return f'<_Semaphore value={self.value} waiters={len(self._waiters)}>'
|
||||||
|
|
||||||
def locked(self):
|
def locked(self):
|
||||||
return self.value == 0
|
return self.value == 0
|
||||||
@@ -259,7 +282,7 @@ class MaxConcurrency:
|
|||||||
return self.__class__(self.number, per=self.per, wait=self.wait)
|
return self.__class__(self.number, per=self.per, wait=self.wait)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<MaxConcurrency per={0.per!r} number={0.number} wait={0.wait}>'.format(self)
|
return f'<MaxConcurrency per={self.per!r} number={self.number} wait={self.wait}>'
|
||||||
|
|
||||||
def get_key(self, message):
|
def get_key(self, message):
|
||||||
return self.per.get_key(message)
|
return self.per.get_key(message)
|
||||||
|
|||||||
@@ -22,18 +22,24 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Literal,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import typing
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
import types
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency
|
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
|
||||||
from . import converter as converters
|
from .converter import run_converters, get_converter, Greedy
|
||||||
from ._types import _BaseCommand
|
from ._types import _BaseCommand
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
|
|
||||||
@@ -54,6 +60,7 @@ __all__ = (
|
|||||||
'bot_has_permissions',
|
'bot_has_permissions',
|
||||||
'bot_has_any_role',
|
'bot_has_any_role',
|
||||||
'cooldown',
|
'cooldown',
|
||||||
|
'dynamic_cooldown',
|
||||||
'max_concurrency',
|
'max_concurrency',
|
||||||
'dm_only',
|
'dm_only',
|
||||||
'guild_only',
|
'guild_only',
|
||||||
@@ -63,6 +70,40 @@ __all__ = (
|
|||||||
'bot_has_guild_permissions'
|
'bot_has_guild_permissions'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
partial = functools.partial
|
||||||
|
while True:
|
||||||
|
if hasattr(function, '__wrapped__'):
|
||||||
|
function = function.__wrapped__
|
||||||
|
elif isinstance(function, partial):
|
||||||
|
function = function.func
|
||||||
|
else:
|
||||||
|
return function
|
||||||
|
|
||||||
|
|
||||||
|
def get_signature_parameters(function: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, inspect.Parameter]:
|
||||||
|
signature = inspect.signature(function)
|
||||||
|
params = {}
|
||||||
|
cache: Dict[str, Any] = {}
|
||||||
|
eval_annotation = discord.utils.evaluate_annotation
|
||||||
|
for name, parameter in signature.parameters.items():
|
||||||
|
annotation = parameter.annotation
|
||||||
|
if annotation is parameter.empty:
|
||||||
|
params[name] = parameter
|
||||||
|
continue
|
||||||
|
if annotation is None:
|
||||||
|
params[name] = parameter.replace(annotation=type(None))
|
||||||
|
continue
|
||||||
|
|
||||||
|
annotation = eval_annotation(annotation, globalns, globalns, cache)
|
||||||
|
if annotation is Greedy:
|
||||||
|
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
|
||||||
|
|
||||||
|
params[name] = parameter.replace(annotation=annotation)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def wrap_callback(coro):
|
def wrap_callback(coro):
|
||||||
@functools.wraps(coro)
|
@functools.wraps(coro)
|
||||||
async def wrapped(*args, **kwargs):
|
async def wrapped(*args, **kwargs):
|
||||||
@@ -99,14 +140,6 @@ def hooked_wrapped_callback(command, ctx, coro):
|
|||||||
return ret
|
return ret
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def _convert_to_bool(argument):
|
|
||||||
lowered = argument.lower()
|
|
||||||
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
|
||||||
return True
|
|
||||||
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
raise BadBoolArgument(lowered)
|
|
||||||
|
|
||||||
class _CaseInsensitiveDict(dict):
|
class _CaseInsensitiveDict(dict):
|
||||||
def __contains__(self, k):
|
def __contains__(self, k):
|
||||||
@@ -152,8 +185,8 @@ class Command(_BaseCommand):
|
|||||||
If the command is invoked while it is disabled, then
|
If the command is invoked while it is disabled, then
|
||||||
:exc:`.DisabledCommand` is raised to the :func:`.on_command_error`
|
:exc:`.DisabledCommand` is raised to the :func:`.on_command_error`
|
||||||
event. Defaults to ``True``.
|
event. Defaults to ``True``.
|
||||||
parent: Optional[:class:`Command`]
|
parent: Optional[:class:`Group`]
|
||||||
The parent command that this command belongs to. ``None`` if there
|
The parent group that this command belongs to. ``None`` if there
|
||||||
isn't one.
|
isn't one.
|
||||||
cog: Optional[:class:`Cog`]
|
cog: Optional[:class:`Cog`]
|
||||||
The cog that this command belongs to. ``None`` if there isn't one.
|
The cog that this command belongs to. ``None`` if there isn't one.
|
||||||
@@ -193,6 +226,14 @@ class Command(_BaseCommand):
|
|||||||
If ``True``\, cooldown processing is done after argument parsing,
|
If ``True``\, cooldown processing is done after argument parsing,
|
||||||
which calls converters. If ``False`` then cooldown processing is done
|
which calls converters. If ``False`` then cooldown processing is done
|
||||||
first and then the converters are called second. Defaults to ``False``.
|
first and then the converters are called second. Defaults to ``False``.
|
||||||
|
extras: :class:`dict`
|
||||||
|
A dict of user provided extras to attach to the Command.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This object may be copied by the library.
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
@@ -236,6 +277,7 @@ class Command(_BaseCommand):
|
|||||||
self.usage = kwargs.get('usage')
|
self.usage = kwargs.get('usage')
|
||||||
self.rest_is_raw = kwargs.get('rest_is_raw', False)
|
self.rest_is_raw = kwargs.get('rest_is_raw', False)
|
||||||
self.aliases = kwargs.get('aliases', [])
|
self.aliases = kwargs.get('aliases', [])
|
||||||
|
self.extras = kwargs.get('extras', {})
|
||||||
|
|
||||||
if not isinstance(self.aliases, (list, tuple)):
|
if not isinstance(self.aliases, (list, tuple)):
|
||||||
raise TypeError("Aliases of a command must be a list or a tuple of strings.")
|
raise TypeError("Aliases of a command must be a list or a tuple of strings.")
|
||||||
@@ -256,7 +298,10 @@ class Command(_BaseCommand):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
cooldown = kwargs.get('cooldown')
|
cooldown = kwargs.get('cooldown')
|
||||||
finally:
|
finally:
|
||||||
self._buckets = CooldownMapping(cooldown)
|
if cooldown is None:
|
||||||
|
self._buckets = CooldownMapping(cooldown, BucketType.default)
|
||||||
|
elif isinstance(cooldown, CooldownMapping):
|
||||||
|
self._buckets = cooldown
|
||||||
|
|
||||||
try:
|
try:
|
||||||
max_concurrency = func.__commands_max_concurrency__
|
max_concurrency = func.__commands_max_concurrency__
|
||||||
@@ -295,41 +340,15 @@ class Command(_BaseCommand):
|
|||||||
@callback.setter
|
@callback.setter
|
||||||
def callback(self, function):
|
def callback(self, function):
|
||||||
self._callback = function
|
self._callback = function
|
||||||
self.module = function.__module__
|
unwrap = unwrap_function(function)
|
||||||
|
self.module = unwrap.__module__
|
||||||
signature = inspect.signature(function)
|
|
||||||
self.params = signature.parameters.copy()
|
|
||||||
|
|
||||||
# see: https://bugs.python.org/issue41341
|
|
||||||
resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
|
globalns = unwrap.__globals__
|
||||||
except NameError as e:
|
except AttributeError:
|
||||||
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
|
globalns = {}
|
||||||
|
|
||||||
for key, value in self.params.items():
|
self.params = get_signature_parameters(function, globalns)
|
||||||
# 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):
|
def add_check(self, func):
|
||||||
"""Adds a check to the command.
|
"""Adds a check to the command.
|
||||||
@@ -451,92 +470,17 @@ class Command(_BaseCommand):
|
|||||||
finally:
|
finally:
|
||||||
ctx.bot.dispatch('command_error', ctx, error)
|
ctx.bot.dispatch('command_error', ctx, error)
|
||||||
|
|
||||||
async def _actual_conversion(self, ctx, converter, argument, param):
|
|
||||||
if converter is bool:
|
|
||||||
return _convert_to_bool(argument)
|
|
||||||
|
|
||||||
try:
|
|
||||||
module = converter.__module__
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
|
|
||||||
converter = getattr(converters, converter.__name__ + 'Converter', converter)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
|
|
||||||
if inspect.ismethod(converter.convert):
|
|
||||||
return await converter.convert(ctx, argument)
|
|
||||||
else:
|
|
||||||
return await converter().convert(ctx, argument)
|
|
||||||
elif isinstance(converter, converters.Converter):
|
|
||||||
return await converter.convert(ctx, argument)
|
|
||||||
except CommandError:
|
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
raise ConversionError(converter, exc) from exc
|
|
||||||
|
|
||||||
try:
|
|
||||||
return converter(argument)
|
|
||||||
except CommandError:
|
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
try:
|
|
||||||
name = converter.__name__
|
|
||||||
except AttributeError:
|
|
||||||
name = converter.__class__.__name__
|
|
||||||
|
|
||||||
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
|
||||||
|
|
||||||
async def do_conversion(self, ctx, converter, argument, param):
|
|
||||||
try:
|
|
||||||
origin = converter.__origin__
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if origin is typing.Union:
|
|
||||||
errors = []
|
|
||||||
_NoneType = type(None)
|
|
||||||
for conv in converter.__args__:
|
|
||||||
# if we got to this part in the code, then the previous conversions have failed
|
|
||||||
# so we should just undo the view, return the default, and allow parsing to continue
|
|
||||||
# with the other parameters
|
|
||||||
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
|
|
||||||
ctx.view.undo()
|
|
||||||
return None if param.default is param.empty else param.default
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = await self._actual_conversion(ctx, conv, argument, param)
|
|
||||||
except CommandError as exc:
|
|
||||||
errors.append(exc)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# if we're here, then we failed all the converters
|
|
||||||
raise BadUnionArgument(param, converter.__args__, errors)
|
|
||||||
|
|
||||||
return await self._actual_conversion(ctx, converter, argument, param)
|
|
||||||
|
|
||||||
def _get_converter(self, param):
|
|
||||||
converter = param.annotation
|
|
||||||
if converter is param.empty:
|
|
||||||
if param.default is not param.empty:
|
|
||||||
converter = str if param.default is None else type(param.default)
|
|
||||||
else:
|
|
||||||
converter = str
|
|
||||||
return converter
|
|
||||||
|
|
||||||
async def transform(self, ctx, param):
|
async def transform(self, ctx, param):
|
||||||
required = param.default is param.empty
|
required = param.default is param.empty
|
||||||
converter = self._get_converter(param)
|
converter = get_converter(param)
|
||||||
consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
|
consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
|
|
||||||
# The greedy converter is simple -- it keeps going until it fails in which case,
|
# 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
|
# it undos the view ready for the next parameter to use instead
|
||||||
if type(converter) is converters._Greedy:
|
if isinstance(converter, Greedy):
|
||||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
|
||||||
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
||||||
elif param.kind == param.VAR_POSITIONAL:
|
elif param.kind == param.VAR_POSITIONAL:
|
||||||
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
|
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
|
||||||
@@ -552,6 +496,8 @@ class Command(_BaseCommand):
|
|||||||
if required:
|
if required:
|
||||||
if self._is_typing_optional(param.annotation):
|
if self._is_typing_optional(param.annotation):
|
||||||
return None
|
return None
|
||||||
|
if hasattr(converter, '__commands_is_flag__') and converter._can_be_constructible():
|
||||||
|
return await converter._construct_default(ctx)
|
||||||
raise MissingRequiredArgument(param)
|
raise MissingRequiredArgument(param)
|
||||||
return param.default
|
return param.default
|
||||||
|
|
||||||
@@ -562,7 +508,7 @@ class Command(_BaseCommand):
|
|||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
view.previous = previous
|
view.previous = previous
|
||||||
|
|
||||||
return await self.do_conversion(ctx, converter, argument, param)
|
return await run_converters(ctx, converter, argument, param)
|
||||||
|
|
||||||
async def _transform_greedy_pos(self, ctx, param, required, converter):
|
async def _transform_greedy_pos(self, ctx, param, required, converter):
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
@@ -574,7 +520,7 @@ class Command(_BaseCommand):
|
|||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
try:
|
try:
|
||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
value = await self.do_conversion(ctx, converter, argument, param)
|
value = await run_converters(ctx, converter, argument, param)
|
||||||
except (CommandError, ArgumentParsingError):
|
except (CommandError, ArgumentParsingError):
|
||||||
view.index = previous
|
view.index = previous
|
||||||
break
|
break
|
||||||
@@ -590,7 +536,7 @@ class Command(_BaseCommand):
|
|||||||
previous = view.index
|
previous = view.index
|
||||||
try:
|
try:
|
||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
value = await self.do_conversion(ctx, converter, argument, param)
|
value = await run_converters(ctx, converter, argument, param)
|
||||||
except (CommandError, ArgumentParsingError):
|
except (CommandError, ArgumentParsingError):
|
||||||
view.index = previous
|
view.index = previous
|
||||||
raise RuntimeError() from None # break loop
|
raise RuntimeError() from None # break loop
|
||||||
@@ -598,22 +544,25 @@ class Command(_BaseCommand):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clean_params(self):
|
def clean_params(self) -> Dict[str, inspect.Parameter]:
|
||||||
"""OrderedDict[:class:`str`, :class:`inspect.Parameter`]:
|
"""Dict[:class:`str`, :class:`inspect.Parameter`]:
|
||||||
Retrieves the parameter OrderedDict without the context or self parameters.
|
Retrieves the parameter dictionary without the context or self parameters.
|
||||||
|
|
||||||
Useful for inspecting signature.
|
Useful for inspecting signature.
|
||||||
"""
|
"""
|
||||||
result = self.params.copy()
|
result = self.params.copy()
|
||||||
if self.cog is not None:
|
if self.cog is not None:
|
||||||
# first parameter is self
|
# first parameter is self
|
||||||
result.popitem(last=False)
|
try:
|
||||||
|
del result[next(iter(result))]
|
||||||
|
except StopIteration:
|
||||||
|
raise ValueError("missing 'self' parameter") from None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# first/second parameter is context
|
# first/second parameter is context
|
||||||
result.popitem(last=False)
|
del result[next(iter(result))]
|
||||||
except Exception:
|
except StopIteration:
|
||||||
raise ValueError('Missing context parameter') from None
|
raise ValueError("missing 'context' parameter") from None
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -634,7 +583,7 @@ class Command(_BaseCommand):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def parents(self):
|
def parents(self):
|
||||||
"""List[:class:`Command`]: Retrieves the parents of this command.
|
"""List[:class:`Group`]: Retrieves the parents of this command.
|
||||||
|
|
||||||
If the command has no parents then it returns an empty :class:`list`.
|
If the command has no parents then it returns an empty :class:`list`.
|
||||||
|
|
||||||
@@ -652,7 +601,7 @@ class Command(_BaseCommand):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def root_parent(self):
|
def root_parent(self):
|
||||||
"""Optional[:class:`Command`]: Retrieves the root parent of this command.
|
"""Optional[:class:`Group`]: Retrieves the root parent of this command.
|
||||||
|
|
||||||
If the command has no parents then it returns ``None``.
|
If the command has no parents then it returns ``None``.
|
||||||
|
|
||||||
@@ -695,26 +644,25 @@ class Command(_BaseCommand):
|
|||||||
try:
|
try:
|
||||||
next(iterator)
|
next(iterator)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
fmt = 'Callback for {0.name} command is missing "self" parameter.'
|
raise discord.ClientException(f'Callback for {self.name} command is missing "self" parameter.')
|
||||||
raise discord.ClientException(fmt.format(self))
|
|
||||||
|
|
||||||
# next we have the 'ctx' as the next parameter
|
# next we have the 'ctx' as the next parameter
|
||||||
try:
|
try:
|
||||||
next(iterator)
|
next(iterator)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
fmt = 'Callback for {0.name} command is missing "ctx" parameter.'
|
raise discord.ClientException(f'Callback for {self.name} command is missing "ctx" parameter.')
|
||||||
raise discord.ClientException(fmt.format(self))
|
|
||||||
|
|
||||||
for name, param in iterator:
|
for name, param in iterator:
|
||||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
ctx.current_parameter = param
|
||||||
|
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
|
||||||
transformed = await self.transform(ctx, param)
|
transformed = await self.transform(ctx, param)
|
||||||
args.append(transformed)
|
args.append(transformed)
|
||||||
elif param.kind == param.KEYWORD_ONLY:
|
elif param.kind == param.KEYWORD_ONLY:
|
||||||
# kwarg only param denotes "consume rest" semantics
|
# kwarg only param denotes "consume rest" semantics
|
||||||
if self.rest_is_raw:
|
if self.rest_is_raw:
|
||||||
converter = self._get_converter(param)
|
converter = get_converter(param)
|
||||||
argument = view.read_rest()
|
argument = view.read_rest()
|
||||||
kwargs[name] = await self.do_conversion(ctx, converter, argument, param)
|
kwargs[name] = await run_converters(ctx, converter, argument, param)
|
||||||
else:
|
else:
|
||||||
kwargs[name] = await self.transform(ctx, param)
|
kwargs[name] = await self.transform(ctx, param)
|
||||||
break
|
break
|
||||||
@@ -780,9 +728,10 @@ class Command(_BaseCommand):
|
|||||||
dt = ctx.message.edited_at or ctx.message.created_at
|
dt = ctx.message.edited_at or ctx.message.created_at
|
||||||
current = dt.replace(tzinfo=datetime.timezone.utc).timestamp()
|
current = dt.replace(tzinfo=datetime.timezone.utc).timestamp()
|
||||||
bucket = self._buckets.get_bucket(ctx.message, current)
|
bucket = self._buckets.get_bucket(ctx.message, current)
|
||||||
retry_after = bucket.update_rate_limit(current)
|
if bucket is not None:
|
||||||
if retry_after:
|
retry_after = bucket.update_rate_limit(current)
|
||||||
raise CommandOnCooldown(bucket, retry_after)
|
if retry_after:
|
||||||
|
raise CommandOnCooldown(bucket, retry_after)
|
||||||
|
|
||||||
async def prepare(self, ctx):
|
async def prepare(self, ctx):
|
||||||
ctx.command = self
|
ctx.command = self
|
||||||
@@ -986,9 +935,9 @@ class Command(_BaseCommand):
|
|||||||
def short_doc(self):
|
def short_doc(self):
|
||||||
""":class:`str`: Gets the "short" documentation of a command.
|
""":class:`str`: Gets the "short" documentation of a command.
|
||||||
|
|
||||||
By default, this is the :attr:`brief` attribute.
|
By default, this is the :attr:`.brief` attribute.
|
||||||
If that lookup leads to an empty string then the first line of the
|
If that lookup leads to an empty string then the first line of the
|
||||||
:attr:`help` attribute is used instead.
|
:attr:`.help` attribute is used instead.
|
||||||
"""
|
"""
|
||||||
if self.brief is not None:
|
if self.brief is not None:
|
||||||
return self.brief
|
return self.brief
|
||||||
@@ -997,15 +946,7 @@ class Command(_BaseCommand):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
def _is_typing_optional(self, annotation):
|
def _is_typing_optional(self, annotation):
|
||||||
try:
|
return getattr(annotation, '__origin__', None) is Union and type(None) in annotation.__args__
|
||||||
origin = annotation.__origin__
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if origin is not typing.Union:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return annotation.__args__[-1] is type(None)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signature(self):
|
def signature(self):
|
||||||
@@ -1013,15 +954,29 @@ class Command(_BaseCommand):
|
|||||||
if self.usage is not None:
|
if self.usage is not None:
|
||||||
return self.usage
|
return self.usage
|
||||||
|
|
||||||
|
|
||||||
params = self.clean_params
|
params = self.clean_params
|
||||||
if not params:
|
if not params:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for name, param in params.items():
|
for name, param in params.items():
|
||||||
greedy = isinstance(param.annotation, converters._Greedy)
|
greedy = isinstance(param.annotation, Greedy)
|
||||||
|
optional = False # postpone evaluation of if it's an optional argument
|
||||||
|
|
||||||
|
# for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the
|
||||||
|
# parameter signature is a literal list of it's values
|
||||||
|
annotation = param.annotation.converter if greedy else param.annotation
|
||||||
|
origin = getattr(annotation, '__origin__', None)
|
||||||
|
if not greedy and origin is Union:
|
||||||
|
none_cls = type(None)
|
||||||
|
union_args = annotation.__args__
|
||||||
|
optional = union_args[-1] is none_cls
|
||||||
|
if len(union_args) == 2 and optional:
|
||||||
|
annotation = union_args[0]
|
||||||
|
origin = getattr(annotation, '__origin__', None)
|
||||||
|
|
||||||
|
if origin is Literal:
|
||||||
|
name = '|'.join(f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__)
|
||||||
if param.default is not param.empty:
|
if param.default is not param.empty:
|
||||||
# We don't want None or '' to trigger the [name=value] case and instead it should
|
# We don't want None or '' to trigger the [name=value] case and instead it should
|
||||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
||||||
@@ -1040,7 +995,7 @@ class Command(_BaseCommand):
|
|||||||
result.append(f'[{name}...]')
|
result.append(f'[{name}...]')
|
||||||
elif greedy:
|
elif greedy:
|
||||||
result.append(f'[{name}]...')
|
result.append(f'[{name}]...')
|
||||||
elif self._is_typing_optional(param.annotation):
|
elif optional:
|
||||||
result.append(f'[{name}]')
|
result.append(f'[{name}]')
|
||||||
else:
|
else:
|
||||||
result.append(f'<{name}>')
|
result.append(f'<{name}>')
|
||||||
@@ -1051,7 +1006,7 @@ class Command(_BaseCommand):
|
|||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Checks if the command can be executed by checking all the predicates
|
Checks if the command can be executed by checking all the predicates
|
||||||
inside the :attr:`checks` attribute. This also checks whether the
|
inside the :attr:`~Command.checks` attribute. This also checks whether the
|
||||||
command is disabled.
|
command is disabled.
|
||||||
|
|
||||||
.. versionchanged:: 1.3
|
.. versionchanged:: 1.3
|
||||||
@@ -1981,9 +1936,48 @@ def cooldown(rate, per, type=BucketType.default):
|
|||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
if isinstance(func, Command):
|
if isinstance(func, Command):
|
||||||
func._buckets = CooldownMapping(Cooldown(rate, per, type))
|
func._buckets = CooldownMapping(Cooldown(rate, per), type)
|
||||||
else:
|
else:
|
||||||
func.__commands_cooldown__ = Cooldown(rate, per, type)
|
func.__commands_cooldown__ = CooldownMapping(Cooldown(rate, per), type)
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def dynamic_cooldown(cooldown, type=BucketType.default):
|
||||||
|
"""A decorator that adds a dynamic cooldown to a :class:`.Command`
|
||||||
|
|
||||||
|
This differs from :func:`.cooldown` in that it takes a function that
|
||||||
|
accepts a single parameter of type :class:`.discord.Message` and must
|
||||||
|
return a :class:`.Cooldown`
|
||||||
|
|
||||||
|
A cooldown allows a command to only be used a specific amount
|
||||||
|
of times in a specific time frame. These cooldowns can be based
|
||||||
|
either on a per-guild, per-channel, per-user, per-role or global basis.
|
||||||
|
Denoted by the third argument of ``type`` which must be of enum
|
||||||
|
type :class:`.BucketType`.
|
||||||
|
|
||||||
|
If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in
|
||||||
|
:func:`.on_command_error` and the local error handler.
|
||||||
|
|
||||||
|
A command can only have a single cooldown.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
cooldown: Callable[[:class:`.discord.Message`], :class:`.Cooldown`]
|
||||||
|
A function that takes a message and returns a cooldown that will
|
||||||
|
apply to this invocation
|
||||||
|
type: :class:`.BucketType`
|
||||||
|
The type of cooldown to have.
|
||||||
|
"""
|
||||||
|
if not callable(cooldown):
|
||||||
|
raise TypeError("A callable must be provided")
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if isinstance(func, Command):
|
||||||
|
func._buckets = DynamicCooldownMapping(cooldown, type)
|
||||||
|
else:
|
||||||
|
func.__commands_cooldown__ = DynamicCooldownMapping(cooldown, type)
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@@ -2046,7 +2040,7 @@ def before_invoke(coro):
|
|||||||
@commands.before_invoke(record_usage)
|
@commands.before_invoke(record_usage)
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def when(self, ctx): # Output: <User> used when at <Time>
|
async def when(self, ctx): # Output: <User> used when at <Time>
|
||||||
await ctx.send('and i have existed since {}'.format(ctx.bot.user.created_at))
|
await ctx.send(f'and i have existed since {ctx.bot.user.created_at}')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def where(self, ctx): # Output: <Nothing>
|
async def where(self, ctx): # Output: <Nothing>
|
||||||
|
|||||||
@@ -42,12 +42,14 @@ __all__ = (
|
|||||||
'MaxConcurrencyReached',
|
'MaxConcurrencyReached',
|
||||||
'NotOwner',
|
'NotOwner',
|
||||||
'MessageNotFound',
|
'MessageNotFound',
|
||||||
|
'ObjectNotFound',
|
||||||
'MemberNotFound',
|
'MemberNotFound',
|
||||||
'GuildNotFound',
|
'GuildNotFound',
|
||||||
'UserNotFound',
|
'UserNotFound',
|
||||||
'ChannelNotFound',
|
'ChannelNotFound',
|
||||||
'ChannelNotReadable',
|
'ChannelNotReadable',
|
||||||
'BadColourArgument',
|
'BadColourArgument',
|
||||||
|
'BadColorArgument',
|
||||||
'RoleNotFound',
|
'RoleNotFound',
|
||||||
'BadInviteArgument',
|
'BadInviteArgument',
|
||||||
'EmojiNotFound',
|
'EmojiNotFound',
|
||||||
@@ -62,6 +64,7 @@ __all__ = (
|
|||||||
'NSFWChannelRequired',
|
'NSFWChannelRequired',
|
||||||
'ConversionError',
|
'ConversionError',
|
||||||
'BadUnionArgument',
|
'BadUnionArgument',
|
||||||
|
'BadLiteralArgument',
|
||||||
'ArgumentParsingError',
|
'ArgumentParsingError',
|
||||||
'UnexpectedQuoteError',
|
'UnexpectedQuoteError',
|
||||||
'InvalidEndOfQuotedStringError',
|
'InvalidEndOfQuotedStringError',
|
||||||
@@ -73,6 +76,11 @@ __all__ = (
|
|||||||
'ExtensionFailed',
|
'ExtensionFailed',
|
||||||
'ExtensionNotFound',
|
'ExtensionNotFound',
|
||||||
'CommandRegistrationError',
|
'CommandRegistrationError',
|
||||||
|
'FlagError',
|
||||||
|
'BadFlagArgument',
|
||||||
|
'MissingFlagArgument',
|
||||||
|
'TooManyFlags',
|
||||||
|
'MissingRequiredFlag',
|
||||||
)
|
)
|
||||||
|
|
||||||
class CommandError(DiscordException):
|
class CommandError(DiscordException):
|
||||||
@@ -82,7 +90,7 @@ class CommandError(DiscordException):
|
|||||||
|
|
||||||
This exception and exceptions inherited from it are handled
|
This exception and exceptions inherited from it are handled
|
||||||
in a special way as they are caught and passed into a special event
|
in a special way as they are caught and passed into a special event
|
||||||
from :class:`.Bot`\, :func:`on_command_error`.
|
from :class:`.Bot`\, :func:`.on_command_error`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, message=None, *args):
|
def __init__(self, message=None, *args):
|
||||||
if message is not None:
|
if message is not None:
|
||||||
@@ -212,6 +220,23 @@ class NotOwner(CheckFailure):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ObjectNotFound(BadArgument):
|
||||||
|
"""Exception raised when the argument provided did not match the format
|
||||||
|
of an ID or a mention.
|
||||||
|
|
||||||
|
This inherits from :exc:`BadArgument`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
argument: :class:`str`
|
||||||
|
The argument supplied by the caller that was not matched
|
||||||
|
"""
|
||||||
|
def __init__(self, argument):
|
||||||
|
self.argument = argument
|
||||||
|
super().__init__(f'{argument!r} does not follow a valid ID or mention format.')
|
||||||
|
|
||||||
class MemberNotFound(BadArgument):
|
class MemberNotFound(BadArgument):
|
||||||
"""Exception raised when the member provided was not found in the bot's
|
"""Exception raised when the member provided was not found in the bot's
|
||||||
cache.
|
cache.
|
||||||
@@ -424,7 +449,7 @@ class CommandInvokeError(CommandError):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, e):
|
def __init__(self, e):
|
||||||
self.original = e
|
self.original = e
|
||||||
super().__init__('Command raised an exception: {0.__class__.__name__}: {0}'.format(e))
|
super().__init__(f'Command raised an exception: {e.__class__.__name__}: {e}')
|
||||||
|
|
||||||
class CommandOnCooldown(CommandError):
|
class CommandOnCooldown(CommandError):
|
||||||
"""Exception raised when the command being invoked is on cooldown.
|
"""Exception raised when the command being invoked is on cooldown.
|
||||||
@@ -433,7 +458,7 @@ class CommandOnCooldown(CommandError):
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
cooldown: Cooldown
|
cooldown: ``Cooldown``
|
||||||
A class with attributes ``rate``, ``per``, and ``type`` similar to
|
A class with attributes ``rate``, ``per``, and ``type`` similar to
|
||||||
the :func:`.cooldown` decorator.
|
the :func:`.cooldown` decorator.
|
||||||
retry_after: :class:`float`
|
retry_after: :class:`float`
|
||||||
@@ -464,7 +489,7 @@ class MaxConcurrencyReached(CommandError):
|
|||||||
suffix = 'per %s' % name if per.name != 'default' else 'globally'
|
suffix = 'per %s' % name if per.name != 'default' else 'globally'
|
||||||
plural = '%s times %s' if number > 1 else '%s time %s'
|
plural = '%s times %s' if number > 1 else '%s time %s'
|
||||||
fmt = plural % (number, suffix)
|
fmt = plural % (number, suffix)
|
||||||
super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
|
super().__init__(f'Too many people are using this command. It can only be used {fmt} concurrently.')
|
||||||
|
|
||||||
class MissingRole(CheckFailure):
|
class MissingRole(CheckFailure):
|
||||||
"""Exception raised when the command invoker lacks a role to run a command.
|
"""Exception raised when the command invoker lacks a role to run a command.
|
||||||
@@ -630,7 +655,7 @@ class BadUnionArgument(UserInputError):
|
|||||||
-----------
|
-----------
|
||||||
param: :class:`inspect.Parameter`
|
param: :class:`inspect.Parameter`
|
||||||
The parameter that failed being converted.
|
The parameter that failed being converted.
|
||||||
converters: Tuple[Type, ...]
|
converters: Tuple[Type, ``...``]
|
||||||
A tuple of converters attempted in conversion, in order of failure.
|
A tuple of converters attempted in conversion, in order of failure.
|
||||||
errors: List[:class:`CommandError`]
|
errors: List[:class:`CommandError`]
|
||||||
A list of errors that were caught from failing the conversion.
|
A list of errors that were caught from failing the conversion.
|
||||||
@@ -644,6 +669,8 @@ class BadUnionArgument(UserInputError):
|
|||||||
try:
|
try:
|
||||||
return x.__name__
|
return x.__name__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
if hasattr(x, '__origin__'):
|
||||||
|
return repr(x)
|
||||||
return x.__class__.__name__
|
return x.__class__.__name__
|
||||||
|
|
||||||
to_string = [_get_name(x) for x in converters]
|
to_string = [_get_name(x) for x in converters]
|
||||||
@@ -654,6 +681,36 @@ class BadUnionArgument(UserInputError):
|
|||||||
|
|
||||||
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
|
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
|
||||||
|
|
||||||
|
class BadLiteralArgument(UserInputError):
|
||||||
|
"""Exception raised when a :data:`typing.Literal` converter fails for all
|
||||||
|
its associated values.
|
||||||
|
|
||||||
|
This inherits from :exc:`UserInputError`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
param: :class:`inspect.Parameter`
|
||||||
|
The parameter that failed being converted.
|
||||||
|
literals: Tuple[Any, ``...``]
|
||||||
|
A tuple of values compared against in conversion, in order of failure.
|
||||||
|
errors: List[:class:`CommandError`]
|
||||||
|
A list of errors that were caught from failing the conversion.
|
||||||
|
"""
|
||||||
|
def __init__(self, param, literals, errors):
|
||||||
|
self.param = param
|
||||||
|
self.literals = literals
|
||||||
|
self.errors = errors
|
||||||
|
|
||||||
|
to_string = [repr(l) for l in literals]
|
||||||
|
if len(to_string) > 2:
|
||||||
|
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
|
||||||
|
else:
|
||||||
|
fmt = ' or '.join(to_string)
|
||||||
|
|
||||||
|
super().__init__(f'Could not convert "{param.name}" into the literal {fmt}.')
|
||||||
|
|
||||||
class ArgumentParsingError(UserInputError):
|
class ArgumentParsingError(UserInputError):
|
||||||
"""An exception raised when the parser fails to parse a user's input.
|
"""An exception raised when the parser fails to parse a user's input.
|
||||||
|
|
||||||
@@ -764,8 +821,8 @@ class ExtensionFailed(ExtensionError):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, name, original):
|
def __init__(self, name, original):
|
||||||
self.original = original
|
self.original = original
|
||||||
fmt = 'Extension {0!r} raised an error: {1.__class__.__name__}: {1}'
|
msg = f'Extension {name!r} raised an error: {original.__class__.__name__}: {original}'
|
||||||
super().__init__(fmt.format(name, original), name=name)
|
super().__init__(msg, name=name)
|
||||||
|
|
||||||
class ExtensionNotFound(ExtensionError):
|
class ExtensionNotFound(ExtensionError):
|
||||||
"""An exception raised when an extension is not found.
|
"""An exception raised when an extension is not found.
|
||||||
@@ -779,13 +836,10 @@ class ExtensionNotFound(ExtensionError):
|
|||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The extension that had the error.
|
The extension that had the error.
|
||||||
original: :class:`NoneType`
|
|
||||||
Always ``None`` for backwards compatibility.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, original=None):
|
def __init__(self, name):
|
||||||
self.original = None
|
msg = f'Extension {name!r} could not be loaded.'
|
||||||
fmt = 'Extension {0!r} could not be loaded.'
|
super().__init__(msg, name=name)
|
||||||
super().__init__(fmt.format(name), name=name)
|
|
||||||
|
|
||||||
class CommandRegistrationError(ClientException):
|
class CommandRegistrationError(ClientException):
|
||||||
"""An exception raised when the command can't be added
|
"""An exception raised when the command can't be added
|
||||||
@@ -807,3 +861,76 @@ class CommandRegistrationError(ClientException):
|
|||||||
self.alias_conflict = alias_conflict
|
self.alias_conflict = alias_conflict
|
||||||
type_ = 'alias' if alias_conflict else 'command'
|
type_ = 'alias' if alias_conflict else 'command'
|
||||||
super().__init__(f'The {type_} {name} is already an existing command or alias.')
|
super().__init__(f'The {type_} {name} is already an existing command or alias.')
|
||||||
|
|
||||||
|
class FlagError(BadArgument):
|
||||||
|
"""The base exception type for all flag parsing related errors.
|
||||||
|
|
||||||
|
This inherits from :exc:`BadArgument`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TooManyFlags(FlagError):
|
||||||
|
"""An exception raised when a flag has received too many values.
|
||||||
|
|
||||||
|
This inherits from :exc:`FlagError`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
flag: :class:`~discord.ext.commands.Flag`
|
||||||
|
The flag that received too many values.
|
||||||
|
values: List[:class:`str`]
|
||||||
|
The values that were passed.
|
||||||
|
"""
|
||||||
|
def __init__(self, flag, values):
|
||||||
|
self.flag = flag
|
||||||
|
self.values = values
|
||||||
|
super().__init__(f'Too many flag values, expected {flag.max_args} but received {len(values)}.')
|
||||||
|
|
||||||
|
class BadFlagArgument(FlagError):
|
||||||
|
"""An exception raised when a flag failed to convert a value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, flag):
|
||||||
|
self.flag = flag
|
||||||
|
try:
|
||||||
|
name = flag.annotation.__name__
|
||||||
|
except AttributeError:
|
||||||
|
name = flag.annotation.__class__.__name__
|
||||||
|
|
||||||
|
super().__init__(f'Could not convert to {name!r} for flag {flag.name!r}')
|
||||||
|
|
||||||
|
class MissingRequiredFlag(FlagError):
|
||||||
|
"""An exception raised when a required flag was not given.
|
||||||
|
|
||||||
|
This inherits from :exc:`FlagError`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
flag: :class:`~discord.ext.commands.Flag`
|
||||||
|
The required flag that was not found.
|
||||||
|
"""
|
||||||
|
def __init__(self, flag):
|
||||||
|
self.flag = flag
|
||||||
|
super().__init__(f'Flag {flag.name!r} is required and missing')
|
||||||
|
|
||||||
|
class MissingFlagArgument(FlagError):
|
||||||
|
"""An exception raised when a flag did not get a value.
|
||||||
|
|
||||||
|
This inherits from :exc:`FlagError`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
flag: :class:`~discord.ext.commands.Flag`
|
||||||
|
The flag that did not get a value.
|
||||||
|
"""
|
||||||
|
def __init__(self, flag):
|
||||||
|
self.flag = flag
|
||||||
|
super().__init__(f'Flag {flag.name!r} does not have an argument')
|
||||||
|
|||||||
618
discord/ext/commands/flags.py
Normal file
618
discord/ext/commands/flags.py
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
"""
|
||||||
|
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 .errors import (
|
||||||
|
BadFlagArgument,
|
||||||
|
CommandError,
|
||||||
|
MissingFlagArgument,
|
||||||
|
TooManyFlags,
|
||||||
|
MissingRequiredFlag,
|
||||||
|
)
|
||||||
|
|
||||||
|
from discord.utils import resolve_annotation
|
||||||
|
from .view import StringView
|
||||||
|
from .converter import run_converters
|
||||||
|
|
||||||
|
from discord.utils import maybe_coroutine, MISSING
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Pattern,
|
||||||
|
Set,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Tuple,
|
||||||
|
List,
|
||||||
|
Any,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Flag',
|
||||||
|
'flag',
|
||||||
|
'FlagConverter',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Flag:
|
||||||
|
"""Represents a flag parameter for :class:`FlagConverter`.
|
||||||
|
|
||||||
|
The :func:`~discord.ext.commands.flag` function helps
|
||||||
|
create these flag objects, but it is not necessary to
|
||||||
|
do so. These cannot be constructed manually.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the flag.
|
||||||
|
aliases: List[:class:`str`]
|
||||||
|
The aliases of the flag name.
|
||||||
|
attribute: :class:`str`
|
||||||
|
The attribute in the class that corresponds to this flag.
|
||||||
|
default: Any
|
||||||
|
The default value of the flag, if available.
|
||||||
|
annotation: Any
|
||||||
|
The underlying evaluated annotation of the flag.
|
||||||
|
max_args: :class:`int`
|
||||||
|
The maximum number of arguments the flag can accept.
|
||||||
|
A negative value indicates an unlimited amount of arguments.
|
||||||
|
override: :class:`bool`
|
||||||
|
Whether multiple given values overrides the previous value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = MISSING
|
||||||
|
aliases: List[str] = field(default_factory=list)
|
||||||
|
attribute: str = MISSING
|
||||||
|
annotation: Any = MISSING
|
||||||
|
default: Any = MISSING
|
||||||
|
max_args: int = MISSING
|
||||||
|
override: bool = MISSING
|
||||||
|
cast_to_dict: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the flag is required.
|
||||||
|
|
||||||
|
A required flag has no default value.
|
||||||
|
"""
|
||||||
|
return self.default is MISSING
|
||||||
|
|
||||||
|
|
||||||
|
def flag(
|
||||||
|
*,
|
||||||
|
name: str = MISSING,
|
||||||
|
aliases: List[str] = MISSING,
|
||||||
|
default: Any = MISSING,
|
||||||
|
max_args: int = MISSING,
|
||||||
|
override: bool = MISSING,
|
||||||
|
) -> Any:
|
||||||
|
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
|
||||||
|
class attributes.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
name: :class:`str`
|
||||||
|
The flag name. If not given, defaults to the attribute name.
|
||||||
|
aliases: List[:class:`str`]
|
||||||
|
Aliases to the flag name. If not given no aliases are set.
|
||||||
|
default: Any
|
||||||
|
The default parameter. This could be either a value or a callable that takes
|
||||||
|
:class:`Context` as its sole parameter. If not given then it defaults to
|
||||||
|
the default value given to the attribute.
|
||||||
|
max_args: :class:`int`
|
||||||
|
The maximum number of arguments the flag can accept.
|
||||||
|
A negative value indicates an unlimited amount of arguments.
|
||||||
|
The default value depends on the annotation given.
|
||||||
|
override: :class:`bool`
|
||||||
|
Whether multiple given values overrides the previous value. The default
|
||||||
|
value depends on the annotation given.
|
||||||
|
"""
|
||||||
|
return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_flag_name(name: str, forbidden: Set[str]):
|
||||||
|
if not name:
|
||||||
|
raise ValueError('flag names should not be empty')
|
||||||
|
|
||||||
|
for ch in name:
|
||||||
|
if ch.isspace():
|
||||||
|
raise ValueError(f'flag name {name!r} cannot have spaces')
|
||||||
|
if ch == '\\':
|
||||||
|
raise ValueError(f'flag name {name!r} cannot have backslashes')
|
||||||
|
if ch in forbidden:
|
||||||
|
raise ValueError(f'flag name {name!r} cannot have any of {forbidden!r} within them')
|
||||||
|
|
||||||
|
|
||||||
|
def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[str, Any]) -> Dict[str, Flag]:
|
||||||
|
annotations = namespace.get('__annotations__', {})
|
||||||
|
case_insensitive = namespace['__commands_flag_case_insensitive__']
|
||||||
|
flags: Dict[str, Flag] = {}
|
||||||
|
cache: Dict[str, Any] = {}
|
||||||
|
names: Set[str] = set()
|
||||||
|
for name, annotation in annotations.items():
|
||||||
|
flag = namespace.pop(name, MISSING)
|
||||||
|
if isinstance(flag, Flag):
|
||||||
|
flag.annotation = annotation
|
||||||
|
else:
|
||||||
|
flag = Flag(name=name, annotation=annotation, default=flag)
|
||||||
|
|
||||||
|
flag.attribute = name
|
||||||
|
if flag.name is MISSING:
|
||||||
|
flag.name = name
|
||||||
|
|
||||||
|
annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache)
|
||||||
|
|
||||||
|
if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible():
|
||||||
|
flag.default = annotation._construct_default
|
||||||
|
|
||||||
|
if flag.aliases is MISSING:
|
||||||
|
flag.aliases = []
|
||||||
|
|
||||||
|
# Add sensible defaults based off of the type annotation
|
||||||
|
# <type> -> (max_args=1)
|
||||||
|
# List[str] -> (max_args=-1)
|
||||||
|
# Tuple[int, ...] -> (max_args=1)
|
||||||
|
# Dict[K, V] -> (max_args=-1, override=True)
|
||||||
|
# Union[str, int] -> (max_args=1)
|
||||||
|
# Optional[str] -> (default=None, max_args=1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
origin = annotation.__origin__
|
||||||
|
except AttributeError:
|
||||||
|
# A regular type hint
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = 1
|
||||||
|
else:
|
||||||
|
if origin is Union:
|
||||||
|
# typing.Union
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = 1
|
||||||
|
if annotation.__args__[-1] is type(None) and flag.default is MISSING:
|
||||||
|
# typing.Optional
|
||||||
|
flag.default = None
|
||||||
|
elif origin is tuple:
|
||||||
|
# typing.Tuple
|
||||||
|
# tuple parsing is e.g. `flag: peter 20`
|
||||||
|
# for Tuple[str, int] would give you flag: ('peter', 20)
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = 1
|
||||||
|
elif origin is list:
|
||||||
|
# typing.List
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = -1
|
||||||
|
elif origin is dict:
|
||||||
|
# typing.Dict[K, V]
|
||||||
|
# Equivalent to:
|
||||||
|
# typing.List[typing.Tuple[K, V]]
|
||||||
|
flag.cast_to_dict = True
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = -1
|
||||||
|
if flag.override is MISSING:
|
||||||
|
flag.override = True
|
||||||
|
elif origin is Literal:
|
||||||
|
if flag.max_args is MISSING:
|
||||||
|
flag.max_args = 1
|
||||||
|
else:
|
||||||
|
raise TypeError(f'Unsupported typing annotation {annotation!r} for {flag.name!r} flag')
|
||||||
|
|
||||||
|
if flag.override is MISSING:
|
||||||
|
flag.override = False
|
||||||
|
|
||||||
|
# Validate flag names are unique
|
||||||
|
name = flag.name.casefold() if case_insensitive else flag.name
|
||||||
|
if name in names:
|
||||||
|
raise TypeError(f'{flag.name!r} flag conflicts with previous flag or alias.')
|
||||||
|
else:
|
||||||
|
names.add(name)
|
||||||
|
|
||||||
|
for alias in flag.aliases:
|
||||||
|
# Validate alias is unique
|
||||||
|
alias = alias.casefold() if case_insensitive else alias
|
||||||
|
if alias in names:
|
||||||
|
raise TypeError(f'{flag.name!r} flag alias {alias!r} conflicts with previous flag or alias.')
|
||||||
|
else:
|
||||||
|
names.add(alias)
|
||||||
|
|
||||||
|
flags[flag.name] = flag
|
||||||
|
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
class FlagsMeta(type):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
__commands_is_flag__: bool
|
||||||
|
__commands_flags__: Dict[str, Flag]
|
||||||
|
__commands_flag_aliases__: Dict[str, str]
|
||||||
|
__commands_flag_regex__: Pattern[str]
|
||||||
|
__commands_flag_case_insensitive__: bool
|
||||||
|
__commands_flag_delimiter__: str
|
||||||
|
__commands_flag_prefix__: str
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
cls: Type[type],
|
||||||
|
name: str,
|
||||||
|
bases: Tuple[type, ...],
|
||||||
|
attrs: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
case_insensitive: bool = MISSING,
|
||||||
|
delimiter: str = MISSING,
|
||||||
|
prefix: str = MISSING,
|
||||||
|
):
|
||||||
|
attrs['__commands_is_flag__'] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
global_ns = sys.modules[attrs['__module__']].__dict__
|
||||||
|
except KeyError:
|
||||||
|
global_ns = {}
|
||||||
|
|
||||||
|
frame = inspect.currentframe()
|
||||||
|
try:
|
||||||
|
if frame is None:
|
||||||
|
local_ns = {}
|
||||||
|
else:
|
||||||
|
if frame.f_back is None:
|
||||||
|
local_ns = frame.f_locals
|
||||||
|
else:
|
||||||
|
local_ns = frame.f_back.f_locals
|
||||||
|
finally:
|
||||||
|
del frame
|
||||||
|
|
||||||
|
flags: Dict[str, Flag] = {}
|
||||||
|
aliases: Dict[str, str] = {}
|
||||||
|
for base in reversed(bases):
|
||||||
|
if base.__dict__.get('__commands_is_flag__', False):
|
||||||
|
flags.update(base.__dict__['__commands_flags__'])
|
||||||
|
aliases.update(base.__dict__['__commands_flag_aliases__'])
|
||||||
|
if case_insensitive is MISSING:
|
||||||
|
attrs['__commands_flag_case_insensitive__'] = base.__dict__['__commands_flag_case_insensitive__']
|
||||||
|
if delimiter is MISSING:
|
||||||
|
attrs['__commands_flag_delimiter__'] = base.__dict__['__commands_flag_delimiter__']
|
||||||
|
if prefix is MISSING:
|
||||||
|
attrs['__commands_flag_prefix__'] = base.__dict__['__commands_flag_prefix__']
|
||||||
|
|
||||||
|
if case_insensitive is not MISSING:
|
||||||
|
attrs['__commands_flag_case_insensitive__'] = case_insensitive
|
||||||
|
if delimiter is not MISSING:
|
||||||
|
attrs['__commands_flag_delimiter__'] = delimiter
|
||||||
|
if prefix is not MISSING:
|
||||||
|
attrs['__commands_flag_prefix__'] = prefix
|
||||||
|
|
||||||
|
case_insensitive = attrs.setdefault('__commands_flag_case_insensitive__', False)
|
||||||
|
delimiter = attrs.setdefault('__commands_flag_delimiter__', ':')
|
||||||
|
prefix = attrs.setdefault('__commands_flag_prefix__', '')
|
||||||
|
|
||||||
|
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
|
||||||
|
flags[flag_name] = flag
|
||||||
|
aliases.update({alias_name: flag_name for alias_name in flag.aliases})
|
||||||
|
|
||||||
|
forbidden = set(delimiter).union(prefix)
|
||||||
|
for flag_name in flags:
|
||||||
|
validate_flag_name(flag_name, forbidden)
|
||||||
|
for alias_name in aliases:
|
||||||
|
validate_flag_name(alias_name, forbidden)
|
||||||
|
|
||||||
|
regex_flags = 0
|
||||||
|
if case_insensitive:
|
||||||
|
flags = {key.casefold(): value for key, value in flags.items()}
|
||||||
|
aliases = {key.casefold(): value.casefold() for key, value in aliases.items()}
|
||||||
|
regex_flags = re.IGNORECASE
|
||||||
|
|
||||||
|
keys = list(re.escape(k) for k in flags)
|
||||||
|
keys.extend(re.escape(a) for a in aliases)
|
||||||
|
keys = sorted(keys, key=lambda t: len(t), reverse=True)
|
||||||
|
|
||||||
|
joined = '|'.join(keys)
|
||||||
|
pattern = re.compile(f'(({re.escape(prefix)})(?P<flag>{joined}){re.escape(delimiter)})', regex_flags)
|
||||||
|
attrs['__commands_flag_regex__'] = pattern
|
||||||
|
attrs['__commands_flags__'] = flags
|
||||||
|
attrs['__commands_flag_aliases__'] = aliases
|
||||||
|
|
||||||
|
return type.__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
|
async def tuple_convert_all(ctx: Context, argument: str, flag: Flag, converter: Any) -> Tuple[Any, ...]:
|
||||||
|
view = StringView(argument)
|
||||||
|
results = []
|
||||||
|
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||||
|
while not view.eof:
|
||||||
|
view.skip_ws()
|
||||||
|
if view.eof:
|
||||||
|
break
|
||||||
|
|
||||||
|
word = view.get_quoted_word()
|
||||||
|
if word is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
converted = await run_converters(ctx, converter, word, param)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise BadFlagArgument(flag) from e
|
||||||
|
else:
|
||||||
|
results.append(converted)
|
||||||
|
|
||||||
|
return tuple(results)
|
||||||
|
|
||||||
|
|
||||||
|
async def tuple_convert_flag(ctx: Context, argument: str, flag: Flag, converters: Any) -> Tuple[Any, ...]:
|
||||||
|
view = StringView(argument)
|
||||||
|
results = []
|
||||||
|
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||||
|
for converter in converters:
|
||||||
|
view.skip_ws()
|
||||||
|
if view.eof:
|
||||||
|
break
|
||||||
|
|
||||||
|
word = view.get_quoted_word()
|
||||||
|
if word is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
converted = await run_converters(ctx, converter, word, param)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise BadFlagArgument(flag) from e
|
||||||
|
else:
|
||||||
|
results.append(converted)
|
||||||
|
|
||||||
|
if len(results) != len(converters):
|
||||||
|
raise BadFlagArgument(flag)
|
||||||
|
|
||||||
|
return tuple(results)
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_flag(ctx, argument: str, flag: Flag, annotation: Any = None) -> Any:
|
||||||
|
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||||
|
annotation = annotation or flag.annotation
|
||||||
|
try:
|
||||||
|
origin = annotation.__origin__
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if origin is tuple:
|
||||||
|
if annotation.__args__[-1] is Ellipsis:
|
||||||
|
return await tuple_convert_all(ctx, argument, flag, annotation.__args__[0])
|
||||||
|
else:
|
||||||
|
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
|
||||||
|
elif origin is list:
|
||||||
|
# typing.List[x]
|
||||||
|
annotation = annotation.__args__[0]
|
||||||
|
return await convert_flag(ctx, argument, flag, annotation)
|
||||||
|
elif origin is Union and annotation.__args__[-1] is type(None):
|
||||||
|
# typing.Optional[x]
|
||||||
|
annotation = Union[annotation.__args__[:-1]]
|
||||||
|
return await run_converters(ctx, annotation, argument, param)
|
||||||
|
elif origin is dict:
|
||||||
|
# typing.Dict[K, V] -> typing.Tuple[K, V]
|
||||||
|
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await run_converters(ctx, annotation, argument, param)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise BadFlagArgument(flag) from e
|
||||||
|
|
||||||
|
|
||||||
|
F = TypeVar('F', bound='FlagConverter')
|
||||||
|
|
||||||
|
|
||||||
|
class FlagConverter(metaclass=FlagsMeta):
|
||||||
|
"""A converter that allows for a user-friendly flag syntax.
|
||||||
|
|
||||||
|
The flags are defined using :pep:`526` type annotations similar
|
||||||
|
to the :mod:`dataclasses` Python module. For more information on
|
||||||
|
how this converter works, check the appropriate
|
||||||
|
:ref:`documentation <ext_commands_flag_converter>`.
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: iter(x)
|
||||||
|
|
||||||
|
Returns an iterator of ``(flag_name, flag_value)`` pairs. This allows it
|
||||||
|
to be, for example, constructed as a dict or a list of pairs.
|
||||||
|
Note that aliases are not shown.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
case_insensitive: :class:`bool`
|
||||||
|
A class parameter to toggle case insensitivity of the flag parsing.
|
||||||
|
If ``True`` then flags are parsed in a case insensitive manner.
|
||||||
|
Defaults to ``False``.
|
||||||
|
prefix: :class:`str`
|
||||||
|
The prefix that all flags must be prefixed with. By default
|
||||||
|
there is no prefix.
|
||||||
|
delimiter: :class:`str`
|
||||||
|
The delimiter that separates a flag's argument from the flag's name.
|
||||||
|
By default this is ``:``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_flags(cls) -> Dict[str, Flag]:
|
||||||
|
"""Dict[:class:`str`, :class:`Flag`]: A mapping of flag name to flag object this converter has."""
|
||||||
|
return cls.__commands_flags__.copy()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _can_be_constructible(cls) -> bool:
|
||||||
|
return all(not flag.required for flag in cls.__commands_flags__.values())
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||||
|
for flag in self.__class__.__commands_flags__.values():
|
||||||
|
yield (flag.name, getattr(self, flag.attribute))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _construct_default(cls: Type[F], ctx: Context) -> F:
|
||||||
|
self: F = cls.__new__(cls)
|
||||||
|
flags = cls.__commands_flags__
|
||||||
|
for flag in flags.values():
|
||||||
|
if callable(flag.default):
|
||||||
|
default = await maybe_coroutine(flag.default, ctx)
|
||||||
|
setattr(self, flag.attribute, default)
|
||||||
|
else:
|
||||||
|
setattr(self, flag.attribute, flag.default)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
pairs = ' '.join([f'{flag.attribute}={getattr(self, flag.attribute)!r}' for flag in self.get_flags().values()])
|
||||||
|
return f'<{self.__class__.__name__} {pairs}>'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_flags(cls, argument: str) -> Dict[str, List[str]]:
|
||||||
|
result: Dict[str, List[str]] = {}
|
||||||
|
flags = cls.__commands_flags__
|
||||||
|
aliases = cls.__commands_flag_aliases__
|
||||||
|
last_position = 0
|
||||||
|
last_flag: Optional[Flag] = None
|
||||||
|
|
||||||
|
case_insensitive = cls.__commands_flag_case_insensitive__
|
||||||
|
for match in cls.__commands_flag_regex__.finditer(argument):
|
||||||
|
begin, end = match.span(0)
|
||||||
|
key = match.group('flag')
|
||||||
|
if case_insensitive:
|
||||||
|
key = key.casefold()
|
||||||
|
|
||||||
|
if key in aliases:
|
||||||
|
key = aliases[key]
|
||||||
|
|
||||||
|
flag = flags.get(key)
|
||||||
|
if last_position and last_flag is not None:
|
||||||
|
value = argument[last_position : begin - 1].lstrip()
|
||||||
|
if not value:
|
||||||
|
raise MissingFlagArgument(last_flag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
values = result[last_flag.name]
|
||||||
|
except KeyError:
|
||||||
|
result[last_flag.name] = [value]
|
||||||
|
else:
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
last_position = end
|
||||||
|
last_flag = flag
|
||||||
|
|
||||||
|
# Add the remaining string to the last available flag
|
||||||
|
if last_position and last_flag is not None:
|
||||||
|
value = argument[last_position:].strip()
|
||||||
|
if not value:
|
||||||
|
raise MissingFlagArgument(last_flag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
values = result[last_flag.name]
|
||||||
|
except KeyError:
|
||||||
|
result[last_flag.name] = [value]
|
||||||
|
else:
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
# Verification of values will come at a later stage
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls: Type[F], ctx: Context, argument: str) -> F:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
The method that actually converters an argument to the flag mapping.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cls: Type[:class:`FlagConverter`]
|
||||||
|
The flag converter class.
|
||||||
|
ctx: :class:`Context`
|
||||||
|
The invocation context.
|
||||||
|
argument: :class:`str`
|
||||||
|
The argument to convert from.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
--------
|
||||||
|
FlagError
|
||||||
|
A flag related parsing error.
|
||||||
|
CommandError
|
||||||
|
A command related error.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`FlagConverter`
|
||||||
|
The flag converter instance with all flags parsed.
|
||||||
|
"""
|
||||||
|
arguments = cls.parse_flags(argument)
|
||||||
|
flags = cls.__commands_flags__
|
||||||
|
|
||||||
|
self: F = cls.__new__(cls)
|
||||||
|
for name, flag in flags.items():
|
||||||
|
try:
|
||||||
|
values = arguments[name]
|
||||||
|
except KeyError:
|
||||||
|
if flag.required:
|
||||||
|
raise MissingRequiredFlag(flag)
|
||||||
|
else:
|
||||||
|
if callable(flag.default):
|
||||||
|
default = await maybe_coroutine(flag.default, ctx)
|
||||||
|
setattr(self, flag.attribute, default)
|
||||||
|
else:
|
||||||
|
setattr(self, flag.attribute, flag.default)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if flag.max_args > 0 and len(values) > flag.max_args:
|
||||||
|
if flag.override:
|
||||||
|
values = values[-flag.max_args :]
|
||||||
|
else:
|
||||||
|
raise TooManyFlags(flag, values)
|
||||||
|
|
||||||
|
# Special case:
|
||||||
|
if flag.max_args == 1:
|
||||||
|
value = await convert_flag(ctx, values[0], flag)
|
||||||
|
setattr(self, flag.attribute, value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Another special case, tuple parsing.
|
||||||
|
# Tuple parsing is basically converting arguments within the flag
|
||||||
|
# So, given flag: hello 20 as the input and Tuple[str, int] as the type hint
|
||||||
|
# We would receive ('hello', 20) as the resulting value
|
||||||
|
# This uses the same whitespace and quoting rules as regular parameters.
|
||||||
|
values = [await convert_flag(ctx, value, flag) for value in values]
|
||||||
|
|
||||||
|
if flag.cast_to_dict:
|
||||||
|
values = dict(values) # type: ignore
|
||||||
|
|
||||||
|
setattr(self, flag.attribute, values)
|
||||||
|
|
||||||
|
return self
|
||||||
@@ -60,6 +60,7 @@ __all__ = (
|
|||||||
# Type <prefix>help command for more info on a command.
|
# Type <prefix>help command for more info on a command.
|
||||||
# You can also type <prefix>help category for more info on a category.
|
# You can also type <prefix>help category for more info on a category.
|
||||||
|
|
||||||
|
|
||||||
class Paginator:
|
class Paginator:
|
||||||
"""A class that aids in paginating code blocks for Discord messages.
|
"""A class that aids in paginating code blocks for Discord messages.
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class Paginator:
|
|||||||
The character string inserted between lines. e.g. a newline character.
|
The character string inserted between lines. e.g. a newline character.
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
self.suffix = suffix
|
self.suffix = suffix
|
||||||
@@ -92,7 +94,7 @@ class Paginator:
|
|||||||
"""Clears the paginator to have no pages."""
|
"""Clears the paginator to have no pages."""
|
||||||
if self.prefix is not None:
|
if self.prefix is not None:
|
||||||
self._current_page = [self.prefix]
|
self._current_page = [self.prefix]
|
||||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||||
else:
|
else:
|
||||||
self._current_page = []
|
self._current_page = []
|
||||||
self._count = 0
|
self._count = 0
|
||||||
@@ -150,7 +152,7 @@ class Paginator:
|
|||||||
|
|
||||||
if self.prefix is not None:
|
if self.prefix is not None:
|
||||||
self._current_page = [self.prefix]
|
self._current_page = [self.prefix]
|
||||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||||
else:
|
else:
|
||||||
self._current_page = []
|
self._current_page = []
|
||||||
self._count = 0
|
self._count = 0
|
||||||
@@ -171,10 +173,12 @@ class Paginator:
|
|||||||
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} 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)
|
return fmt.format(self)
|
||||||
|
|
||||||
|
|
||||||
def _not_overriden(f):
|
def _not_overriden(f):
|
||||||
f.__help_command_not_overriden__ = True
|
f.__help_command_not_overriden__ = True
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
class _HelpCommandImpl(Command):
|
class _HelpCommandImpl(Command):
|
||||||
def __init__(self, inject, *args, **kwargs):
|
def __init__(self, inject, *args, **kwargs):
|
||||||
super().__init__(inject.command_callback, *args, **kwargs)
|
super().__init__(inject.command_callback, *args, **kwargs)
|
||||||
@@ -212,8 +216,8 @@ class _HelpCommandImpl(Command):
|
|||||||
def clean_params(self):
|
def clean_params(self):
|
||||||
result = self.params.copy()
|
result = self.params.copy()
|
||||||
try:
|
try:
|
||||||
result.popitem(last=False)
|
del result[next(iter(result))]
|
||||||
except Exception:
|
except StopIteration:
|
||||||
raise ValueError('Missing context parameter') from None
|
raise ValueError('Missing context parameter') from None
|
||||||
else:
|
else:
|
||||||
return result
|
return result
|
||||||
@@ -250,6 +254,7 @@ class _HelpCommandImpl(Command):
|
|||||||
cog.walk_commands = cog.walk_commands.__wrapped__
|
cog.walk_commands = cog.walk_commands.__wrapped__
|
||||||
self.cog = None
|
self.cog = None
|
||||||
|
|
||||||
|
|
||||||
class HelpCommand:
|
class HelpCommand:
|
||||||
r"""The base implementation for help command formatting.
|
r"""The base implementation for help command formatting.
|
||||||
|
|
||||||
@@ -272,11 +277,11 @@ class HelpCommand:
|
|||||||
Defaults to ``False``.
|
Defaults to ``False``.
|
||||||
verify_checks: Optional[:class:`bool`]
|
verify_checks: Optional[:class:`bool`]
|
||||||
Specifies if commands should have their :attr:`.Command.checks` called
|
Specifies if commands should have their :attr:`.Command.checks` called
|
||||||
and verified. If ``True``, always calls :attr:`.Commands.checks`.
|
and verified. If ``True``, always calls :attr:`.Command.checks`.
|
||||||
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
|
If ``None``, only calls :attr:`.Command.checks` in a guild setting.
|
||||||
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
|
If ``False``, never calls :attr:`.Command.checks`. Defaults to ``True``.
|
||||||
|
|
||||||
..versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
command_attrs: :class:`dict`
|
command_attrs: :class:`dict`
|
||||||
A dictionary of options to pass in for the construction of the help command.
|
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
|
This allows you to change the command behaviour without actually changing
|
||||||
@@ -288,7 +293,7 @@ class HelpCommand:
|
|||||||
'@everyone': '@\u200beveryone',
|
'@everyone': '@\u200beveryone',
|
||||||
'@here': '@\u200bhere',
|
'@here': '@\u200bhere',
|
||||||
r'<@!?[0-9]{17,22}>': '@deleted-user',
|
r'<@!?[0-9]{17,22}>': '@deleted-user',
|
||||||
r'<@&[0-9]{17,22}>': '@deleted-role'
|
r'<@&[0-9]{17,22}>': '@deleted-role',
|
||||||
}
|
}
|
||||||
|
|
||||||
MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
|
MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
|
||||||
@@ -305,10 +310,7 @@ class HelpCommand:
|
|||||||
# The keys can be safely copied as-is since they're 99.99% certain of being
|
# The keys can be safely copied as-is since they're 99.99% certain of being
|
||||||
# string keys
|
# string keys
|
||||||
deepcopy = copy.deepcopy
|
deepcopy = copy.deepcopy
|
||||||
self.__original_kwargs__ = {
|
self.__original_kwargs__ = {k: deepcopy(v) for k, v in kwargs.items()}
|
||||||
k: deepcopy(v)
|
|
||||||
for k, v in kwargs.items()
|
|
||||||
}
|
|
||||||
self.__original_args__ = deepcopy(args)
|
self.__original_args__ = deepcopy(args)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -369,25 +371,10 @@ class HelpCommand:
|
|||||||
def get_bot_mapping(self):
|
def get_bot_mapping(self):
|
||||||
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
||||||
bot = self.context.bot
|
bot = self.context.bot
|
||||||
mapping = {
|
mapping = {cog: cog.get_commands() for cog in bot.cogs.values()}
|
||||||
cog: cog.get_commands()
|
|
||||||
for cog in bot.cogs.values()
|
|
||||||
}
|
|
||||||
mapping[None] = [c for c in bot.commands if c.cog is None]
|
mapping[None] = [c for c in bot.commands if c.cog is None]
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
@property
|
|
||||||
def clean_prefix(self):
|
|
||||||
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
|
|
||||||
user = self.context.guild.me if self.context.guild else self.context.bot.user
|
|
||||||
# this breaks if the prefix mention is not the bot itself but I
|
|
||||||
# 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(fr"<@!?{user.id}>")
|
|
||||||
display_name = user.display_name.replace('\\', r'\\')
|
|
||||||
return pattern.sub('@' + display_name, self.context.prefix)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invoked_with(self):
|
def invoked_with(self):
|
||||||
"""Similar to :attr:`Context.invoked_with` except properly handles
|
"""Similar to :attr:`Context.invoked_with` except properly handles
|
||||||
@@ -442,7 +429,7 @@ class HelpCommand:
|
|||||||
else:
|
else:
|
||||||
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
||||||
|
|
||||||
return f'{self.clean_prefix}{alias} {command.signature}'
|
return f'{self.context.clean_prefix}{alias} {command.signature}'
|
||||||
|
|
||||||
def remove_mentions(self, string):
|
def remove_mentions(self, string):
|
||||||
"""Removes mentions from the string to prevent abuse.
|
"""Removes mentions from the string to prevent abuse.
|
||||||
@@ -607,10 +594,7 @@ class HelpCommand:
|
|||||||
The maximum width of the commands.
|
The maximum width of the commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
as_lengths = (
|
as_lengths = (discord.utils._string_width(c.name) for c in commands)
|
||||||
discord.utils._string_width(c.name)
|
|
||||||
for c in commands
|
|
||||||
)
|
|
||||||
return max(as_lengths, default=0)
|
return max(as_lengths, default=0)
|
||||||
|
|
||||||
def get_destination(self):
|
def get_destination(self):
|
||||||
@@ -631,8 +615,7 @@ class HelpCommand:
|
|||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Handles the implementation when an error happens in the help command.
|
Handles the implementation when an error happens in the help command.
|
||||||
For example, the result of :meth:`command_not_found` or
|
For example, the result of :meth:`command_not_found` will be passed here.
|
||||||
:meth:`command_has_no_subcommand_found` will be passed here.
|
|
||||||
|
|
||||||
You can override this method to customise the behaviour.
|
You can override this method to customise the behaviour.
|
||||||
|
|
||||||
@@ -880,6 +863,7 @@ class HelpCommand:
|
|||||||
else:
|
else:
|
||||||
return await self.send_command_help(cmd)
|
return await self.send_command_help(cmd)
|
||||||
|
|
||||||
|
|
||||||
class DefaultHelpCommand(HelpCommand):
|
class DefaultHelpCommand(HelpCommand):
|
||||||
"""The implementation of the default help command.
|
"""The implementation of the default help command.
|
||||||
|
|
||||||
@@ -934,14 +918,16 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
def shorten_text(self, text):
|
def shorten_text(self, text):
|
||||||
""":class:`str`: Shortens text to fit into the :attr:`width`."""
|
""":class:`str`: Shortens text to fit into the :attr:`width`."""
|
||||||
if len(text) > self.width:
|
if len(text) > self.width:
|
||||||
return text[:self.width - 3] + '...'
|
return text[:self.width - 3].rstrip() + '...'
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def get_ending_note(self):
|
def get_ending_note(self):
|
||||||
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||||
command_name = self.invoked_with
|
command_name = self.invoked_with
|
||||||
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
|
return (
|
||||||
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
|
f"Type {self.context.clean_prefix}{command_name} command for more info on a command.\n"
|
||||||
|
f"You can also type {self.context.clean_prefix}{command_name} category for more info on a category."
|
||||||
|
)
|
||||||
|
|
||||||
def add_indented_commands(self, commands, *, heading, max_size=None):
|
def add_indented_commands(self, commands, *, heading, max_size=None):
|
||||||
"""Indents a list of commands after the specified heading.
|
"""Indents a list of commands after the specified heading.
|
||||||
@@ -962,7 +948,7 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
if the list of commands is greater than 0.
|
if the list of commands is greater than 0.
|
||||||
max_size: Optional[:class:`int`]
|
max_size: Optional[:class:`int`]
|
||||||
The max size to use for the gap between indents.
|
The max size to use for the gap between indents.
|
||||||
If unspecified, calls :meth:`get_max_size` on the
|
If unspecified, calls :meth:`~HelpCommand.get_max_size` on the
|
||||||
commands parameter.
|
commands parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -1030,6 +1016,7 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
self.paginator.add_line(bot.description, empty=True)
|
self.paginator.add_line(bot.description, empty=True)
|
||||||
|
|
||||||
no_category = f'\u200b{self.no_category}:'
|
no_category = f'\u200b{self.no_category}:'
|
||||||
|
|
||||||
def get_category(command, *, no_category=no_category):
|
def get_category(command, *, no_category=no_category):
|
||||||
cog = command.cog
|
cog = command.cog
|
||||||
return cog.qualified_name + ':' if cog is not None else no_category
|
return cog.qualified_name + ':' if cog is not None else no_category
|
||||||
@@ -1083,6 +1070,7 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
|
|
||||||
await self.send_pages()
|
await self.send_pages()
|
||||||
|
|
||||||
|
|
||||||
class MinimalHelpCommand(HelpCommand):
|
class MinimalHelpCommand(HelpCommand):
|
||||||
"""An implementation of a help command with minimal output.
|
"""An implementation of a help command with minimal output.
|
||||||
|
|
||||||
@@ -1149,11 +1137,13 @@ class MinimalHelpCommand(HelpCommand):
|
|||||||
The help command opening note.
|
The help command opening note.
|
||||||
"""
|
"""
|
||||||
command_name = self.invoked_with
|
command_name = self.invoked_with
|
||||||
return "Use `{0}{1} [command]` for more info on a command.\n" \
|
return (
|
||||||
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
|
f"Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n"
|
||||||
|
f"You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category."
|
||||||
|
)
|
||||||
|
|
||||||
def get_command_signature(self, command):
|
def get_command_signature(self, command):
|
||||||
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
|
return f'{self.context.clean_prefix}{command.qualified_name} {command.signature}'
|
||||||
|
|
||||||
def get_ending_note(self):
|
def get_ending_note(self):
|
||||||
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
||||||
@@ -1202,7 +1192,7 @@ class MinimalHelpCommand(HelpCommand):
|
|||||||
The command to show information of.
|
The command to show information of.
|
||||||
"""
|
"""
|
||||||
fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
|
fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
|
||||||
self.paginator.add_line(fmt.format(self.clean_prefix, command.qualified_name, command.short_doc))
|
self.paginator.add_line(fmt.format(self.context.clean_prefix, command.qualified_name, command.short_doc))
|
||||||
|
|
||||||
def add_aliases_formatting(self, aliases):
|
def add_aliases_formatting(self, aliases):
|
||||||
"""Adds the formatting information on a command's aliases.
|
"""Adds the formatting information on a command's aliases.
|
||||||
@@ -1273,6 +1263,7 @@ class MinimalHelpCommand(HelpCommand):
|
|||||||
self.paginator.add_line(note, empty=True)
|
self.paginator.add_line(note, empty=True)
|
||||||
|
|
||||||
no_category = f'\u200b{self.no_category}'
|
no_category = f'\u200b{self.no_category}'
|
||||||
|
|
||||||
def get_category(command, *, no_category=no_category):
|
def get_category(command, *, no_category=no_category):
|
||||||
cog = command.cog
|
cog = command.cog
|
||||||
return cog.qualified_name if cog is not None else no_category
|
return cog.qualified_name if cog is not None else no_category
|
||||||
|
|||||||
@@ -189,4 +189,4 @@ class StringView:
|
|||||||
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.format(self)
|
return f'<StringView pos: {self.index} prev: {self.previous} end: {self.end} eof: {self.eof}>'
|
||||||
|
|||||||
@@ -22,8 +22,25 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
Generic,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
import inspect
|
import inspect
|
||||||
@@ -31,7 +48,16 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from discord.backoff import ExponentialBackoff
|
from discord.backoff import ExponentialBackoff
|
||||||
|
from discord.utils import MISSING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
else:
|
||||||
|
P = TypeVar("P") # hacky runtime fix
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,19 +65,63 @@ __all__ = (
|
|||||||
'loop',
|
'loop',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Loop:
|
C = TypeVar('C')
|
||||||
|
T = TypeVar('T')
|
||||||
|
_coro = Coroutine[Any, Any, T]
|
||||||
|
_func = Callable[..., Awaitable[Any]]
|
||||||
|
FT = TypeVar('FT', bound=_func)
|
||||||
|
ET = TypeVar('ET', bound=Callable[[Any, BaseException], Awaitable[Any]])
|
||||||
|
LT = TypeVar('LT', bound='Loop')
|
||||||
|
|
||||||
|
|
||||||
|
class SleepHandle:
|
||||||
|
__slots__ = ('future', 'loop', 'handle')
|
||||||
|
|
||||||
|
def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> None:
|
||||||
|
self.loop = loop
|
||||||
|
self.future = future = loop.create_future()
|
||||||
|
relative_delta = discord.utils.compute_timedelta(dt)
|
||||||
|
self.handle = loop.call_later(relative_delta, future.set_result, True)
|
||||||
|
|
||||||
|
def recalculate(self, dt: datetime.datetime) -> None:
|
||||||
|
self.handle.cancel()
|
||||||
|
relative_delta = discord.utils.compute_timedelta(dt)
|
||||||
|
self.handle = self.loop.call_later(relative_delta, self.future.set_result, True)
|
||||||
|
|
||||||
|
def wait(self) -> asyncio.Future:
|
||||||
|
return self.future
|
||||||
|
|
||||||
|
def done(self) -> bool:
|
||||||
|
return self.future.done()
|
||||||
|
|
||||||
|
def cancel(self) -> None:
|
||||||
|
self.handle.cancel()
|
||||||
|
self.future.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
class Loop(Generic[C, P, T]):
|
||||||
"""A background task helper that abstracts the loop and reconnection logic for you.
|
"""A background task helper that abstracts the loop and reconnection logic for you.
|
||||||
|
|
||||||
The main interface to create this is through :func:`loop`.
|
The main interface to create this is through :func:`loop`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
|
def __init__(self,
|
||||||
self.coro = coro
|
coro: Callable[P, _coro[T]],
|
||||||
self.reconnect = reconnect
|
seconds: float,
|
||||||
self.loop = loop
|
hours: float,
|
||||||
self.count = count
|
minutes: float,
|
||||||
|
time: Union[datetime.time, Sequence[datetime.time]],
|
||||||
|
count: Optional[int],
|
||||||
|
reconnect: bool,
|
||||||
|
loop: Optional[asyncio.AbstractEventLoop],
|
||||||
|
) -> None:
|
||||||
|
self.coro: Callable[P, _coro[T]] = coro
|
||||||
|
self.reconnect: bool = reconnect
|
||||||
|
self.loop: Optional[asyncio.AbstractEventLoop] = loop
|
||||||
|
self.count: Optional[int] = count
|
||||||
self._current_loop = 0
|
self._current_loop = 0
|
||||||
|
self._handle = None
|
||||||
self._task = None
|
self._task = None
|
||||||
self._injected = None
|
self._injected: Optional[C] = None
|
||||||
self._valid_exception = (
|
self._valid_exception = (
|
||||||
OSError,
|
OSError,
|
||||||
discord.GatewayNotFound,
|
discord.GatewayNotFound,
|
||||||
@@ -69,15 +139,15 @@ class Loop:
|
|||||||
if self.count is not None and self.count <= 0:
|
if self.count is not None and self.count <= 0:
|
||||||
raise ValueError('count must be greater than 0 or None.')
|
raise ValueError('count must be greater than 0 or None.')
|
||||||
|
|
||||||
self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
|
self.change_interval(seconds=seconds, minutes=minutes, hours=hours, time=time)
|
||||||
self._last_iteration_failed = False
|
self._last_iteration_failed = False
|
||||||
self._last_iteration = None
|
self._last_iteration = None
|
||||||
self._next_iteration = None
|
self._next_iteration = None
|
||||||
|
|
||||||
if not inspect.iscoroutinefunction(self.coro):
|
if not inspect.iscoroutinefunction(self.coro):
|
||||||
raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))
|
raise TypeError(f'Expected coroutine function, not {type(self.coro).__name__!r}.')
|
||||||
|
|
||||||
async def _call_loop_function(self, name, *args, **kwargs):
|
async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> None:
|
||||||
coro = getattr(self, '_' + name)
|
coro = getattr(self, '_' + name)
|
||||||
if coro is None:
|
if coro is None:
|
||||||
return
|
return
|
||||||
@@ -87,14 +157,23 @@ class Loop:
|
|||||||
else:
|
else:
|
||||||
await coro(*args, **kwargs)
|
await coro(*args, **kwargs)
|
||||||
|
|
||||||
async def _loop(self, *args, **kwargs):
|
|
||||||
|
def _try_sleep_until(self, dt: datetime.datetime):
|
||||||
|
self._handle = SleepHandle(dt=dt, loop=self.loop) # type: ignore
|
||||||
|
return self._handle.wait()
|
||||||
|
|
||||||
|
async def _loop(self, *args: Any, **kwargs: Any) -> None:
|
||||||
backoff = ExponentialBackoff()
|
backoff = ExponentialBackoff()
|
||||||
await self._call_loop_function('before_loop')
|
await self._call_loop_function('before_loop')
|
||||||
sleep_until = discord.utils.sleep_until
|
|
||||||
self._last_iteration_failed = False
|
self._last_iteration_failed = False
|
||||||
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
|
if self._time is not MISSING:
|
||||||
|
# the time index should be prepared every time the internal loop is started
|
||||||
|
self._prepare_time_index()
|
||||||
|
self._next_iteration = self._get_next_sleep_time()
|
||||||
|
else:
|
||||||
|
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(0) # allows canceling in before_loop
|
await self._try_sleep_until(self._next_iteration)
|
||||||
while True:
|
while True:
|
||||||
if not self._last_iteration_failed:
|
if not self._last_iteration_failed:
|
||||||
self._last_iteration = self._next_iteration
|
self._last_iteration = self._next_iteration
|
||||||
@@ -102,22 +181,27 @@ class Loop:
|
|||||||
try:
|
try:
|
||||||
await self.coro(*args, **kwargs)
|
await self.coro(*args, **kwargs)
|
||||||
self._last_iteration_failed = False
|
self._last_iteration_failed = False
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
if now > self._next_iteration:
|
|
||||||
self._next_iteration = now
|
|
||||||
except self._valid_exception:
|
except self._valid_exception:
|
||||||
self._last_iteration_failed = True
|
self._last_iteration_failed = True
|
||||||
if not self.reconnect:
|
if not self.reconnect:
|
||||||
raise
|
raise
|
||||||
await asyncio.sleep(backoff.delay())
|
await asyncio.sleep(backoff.delay())
|
||||||
else:
|
else:
|
||||||
|
await self._try_sleep_until(self._next_iteration)
|
||||||
|
|
||||||
if self._stop_next_iteration:
|
if self._stop_next_iteration:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
if now > self._next_iteration:
|
||||||
|
self._next_iteration = now
|
||||||
|
if self._time is not MISSING:
|
||||||
|
self._prepare_time_index(now)
|
||||||
|
|
||||||
self._current_loop += 1
|
self._current_loop += 1
|
||||||
if self._current_loop == self.count:
|
if self._current_loop == self.count:
|
||||||
break
|
break
|
||||||
|
|
||||||
await sleep_until(self._next_iteration)
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self._is_being_cancelled = True
|
self._is_being_cancelled = True
|
||||||
raise
|
raise
|
||||||
@@ -127,17 +211,26 @@ class Loop:
|
|||||||
raise exc
|
raise exc
|
||||||
finally:
|
finally:
|
||||||
await self._call_loop_function('after_loop')
|
await self._call_loop_function('after_loop')
|
||||||
|
self._handle.cancel()
|
||||||
self._is_being_cancelled = False
|
self._is_being_cancelled = False
|
||||||
self._current_loop = 0
|
self._current_loop = 0
|
||||||
self._stop_next_iteration = False
|
self._stop_next_iteration = False
|
||||||
self._has_failed = False
|
self._has_failed = False
|
||||||
|
|
||||||
def __get__(self, obj, objtype):
|
def __get__(self, obj: C, objtype: Type[C]) -> Loop[C, P, T]:
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
|
copy = Loop[C, P, T](
|
||||||
count=self.count, reconnect=self.reconnect, loop=self.loop)
|
self.coro,
|
||||||
|
seconds=self._seconds,
|
||||||
|
hours=self._hours,
|
||||||
|
minutes=self._minutes,
|
||||||
|
time=self._time,
|
||||||
|
count=self.count,
|
||||||
|
reconnect=self.reconnect,
|
||||||
|
loop=self.loop,
|
||||||
|
)
|
||||||
copy._injected = obj
|
copy._injected = obj
|
||||||
copy._before_loop = self._before_loop
|
copy._before_loop = self._before_loop
|
||||||
copy._after_loop = self._after_loop
|
copy._after_loop = self._after_loop
|
||||||
@@ -146,12 +239,52 @@ class Loop:
|
|||||||
return copy
|
return copy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_loop(self):
|
def seconds(self) -> Optional[float]:
|
||||||
|
"""Optional[:class:`float`]: Read-only value for the number of seconds
|
||||||
|
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if self._seconds is not MISSING:
|
||||||
|
return self._seconds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def minutes(self) -> Optional[float]:
|
||||||
|
"""Optional[:class:`float`]: Read-only value for the number of minutes
|
||||||
|
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if self._minutes is not MISSING:
|
||||||
|
return self._minutes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hours(self) -> Optional[float]:
|
||||||
|
"""Optional[:class:`float`]: Read-only value for the number of hours
|
||||||
|
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if self._hours is not MISSING:
|
||||||
|
return self._hours
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time(self) -> Optional[List[datetime.time]]:
|
||||||
|
"""Optional[List[:class:`datetime.time`]]: Read-only list for the exact times this loop runs at.
|
||||||
|
``None`` if relative times were passed instead.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if self._time is not MISSING:
|
||||||
|
return self._time.copy()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_loop(self) -> int:
|
||||||
""":class:`int`: The current iteration of the loop."""
|
""":class:`int`: The current iteration of the loop."""
|
||||||
return self._current_loop
|
return self._current_loop
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def next_iteration(self):
|
def next_iteration(self) -> Optional[datetime.datetime]:
|
||||||
"""Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.
|
"""Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
@@ -162,7 +295,7 @@ class Loop:
|
|||||||
return None
|
return None
|
||||||
return self._next_iteration
|
return self._next_iteration
|
||||||
|
|
||||||
async def __call__(self, *args, **kwargs):
|
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
Calls the internal callback that the task holds.
|
Calls the internal callback that the task holds.
|
||||||
@@ -182,7 +315,7 @@ class Loop:
|
|||||||
|
|
||||||
return await self.coro(*args, **kwargs)
|
return await self.coro(*args, **kwargs)
|
||||||
|
|
||||||
def start(self, *args, **kwargs):
|
def start(self, *args: P.args, **kwargs: P.kwargs) -> asyncio.Task:
|
||||||
r"""Starts the internal task in the event loop.
|
r"""Starts the internal task in the event loop.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -215,7 +348,7 @@ class Loop:
|
|||||||
self._task = self.loop.create_task(self._loop(*args, **kwargs))
|
self._task = self.loop.create_task(self._loop(*args, **kwargs))
|
||||||
return self._task
|
return self._task
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
r"""Gracefully stops the task from running.
|
r"""Gracefully stops the task from running.
|
||||||
|
|
||||||
Unlike :meth:`cancel`\, this allows the task to finish its
|
Unlike :meth:`cancel`\, this allows the task to finish its
|
||||||
@@ -236,15 +369,15 @@ class Loop:
|
|||||||
if self._task and not self._task.done():
|
if self._task and not self._task.done():
|
||||||
self._stop_next_iteration = True
|
self._stop_next_iteration = True
|
||||||
|
|
||||||
def _can_be_cancelled(self):
|
def _can_be_cancelled(self) -> bool:
|
||||||
return not self._is_being_cancelled and self._task and not self._task.done()
|
return bool(not self._is_being_cancelled and self._task and not self._task.done())
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
"""Cancels the internal task, if it is running."""
|
"""Cancels the internal task, if it is running."""
|
||||||
if self._can_be_cancelled():
|
if self._can_be_cancelled():
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
|
|
||||||
def restart(self, *args, **kwargs):
|
def restart(self, *args: P.args, **kwargs: P.kwargs) -> None:
|
||||||
r"""A convenience method to restart the internal task.
|
r"""A convenience method to restart the internal task.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -268,7 +401,7 @@ class Loop:
|
|||||||
self._task.add_done_callback(restart_when_over)
|
self._task.add_done_callback(restart_when_over)
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
|
|
||||||
def add_exception_type(self, *exceptions):
|
def add_exception_type(self, *exceptions: Type[BaseException]) -> None:
|
||||||
r"""Adds exception types to be handled during the reconnect logic.
|
r"""Adds exception types to be handled during the reconnect logic.
|
||||||
|
|
||||||
By default the exception types handled are those handled by
|
By default the exception types handled are those handled by
|
||||||
@@ -297,7 +430,7 @@ class Loop:
|
|||||||
|
|
||||||
self._valid_exception = (*self._valid_exception, *exceptions)
|
self._valid_exception = (*self._valid_exception, *exceptions)
|
||||||
|
|
||||||
def clear_exception_types(self):
|
def clear_exception_types(self) -> None:
|
||||||
"""Removes all exception types that are handled.
|
"""Removes all exception types that are handled.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -306,7 +439,7 @@ class Loop:
|
|||||||
"""
|
"""
|
||||||
self._valid_exception = tuple()
|
self._valid_exception = tuple()
|
||||||
|
|
||||||
def remove_exception_type(self, *exceptions):
|
def remove_exception_type(self, *exceptions: Type[BaseException]) -> bool:
|
||||||
r"""Removes exception types from being handled during the reconnect logic.
|
r"""Removes exception types from being handled during the reconnect logic.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -323,34 +456,34 @@ class Loop:
|
|||||||
self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
|
self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
|
||||||
return len(self._valid_exception) == old_length - len(exceptions)
|
return len(self._valid_exception) == old_length - len(exceptions)
|
||||||
|
|
||||||
def get_task(self):
|
def get_task(self) -> Optional[asyncio.Task]:
|
||||||
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
|
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
|
||||||
return self._task
|
return self._task
|
||||||
|
|
||||||
def is_being_cancelled(self):
|
def is_being_cancelled(self) -> bool:
|
||||||
"""Whether the task is being cancelled."""
|
"""Whether the task is being cancelled."""
|
||||||
return self._is_being_cancelled
|
return self._is_being_cancelled
|
||||||
|
|
||||||
def failed(self):
|
def failed(self) -> bool:
|
||||||
""":class:`bool`: Whether the internal task has failed.
|
""":class:`bool`: Whether the internal task has failed.
|
||||||
|
|
||||||
.. versionadded:: 1.2
|
.. versionadded:: 1.2
|
||||||
"""
|
"""
|
||||||
return self._has_failed
|
return self._has_failed
|
||||||
|
|
||||||
def is_running(self):
|
def is_running(self) -> bool:
|
||||||
""":class:`bool`: Check if the task is currently running.
|
""":class:`bool`: Check if the task is currently running.
|
||||||
|
|
||||||
.. versionadded:: 1.4
|
.. versionadded:: 1.4
|
||||||
"""
|
"""
|
||||||
return not bool(self._task.done()) if self._task else False
|
return not bool(self._task.done()) if self._task else False
|
||||||
|
|
||||||
async def _error(self, *args):
|
async def _error(self, *args: Any) -> None:
|
||||||
exception = args[-1]
|
exception: Exception = args[-1]
|
||||||
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
|
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
|
||||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||||
|
|
||||||
def before_loop(self, coro):
|
def before_loop(self, coro: FT) -> FT:
|
||||||
"""A decorator that registers a coroutine to be called before the loop starts running.
|
"""A decorator that registers a coroutine to be called before the loop starts running.
|
||||||
|
|
||||||
This is useful if you want to wait for some bot state before the loop starts,
|
This is useful if you want to wait for some bot state before the loop starts,
|
||||||
@@ -370,12 +503,12 @@ class Loop:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not inspect.iscoroutinefunction(coro):
|
if not inspect.iscoroutinefunction(coro):
|
||||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
self._before_loop = coro
|
self._before_loop = coro
|
||||||
return coro
|
return coro
|
||||||
|
|
||||||
def after_loop(self, coro):
|
def after_loop(self, coro: FT) -> FT:
|
||||||
"""A decorator that register a coroutine to be called after the loop finished running.
|
"""A decorator that register a coroutine to be called after the loop finished running.
|
||||||
|
|
||||||
The coroutine must take no arguments (except ``self`` in a class context).
|
The coroutine must take no arguments (except ``self`` in a class context).
|
||||||
@@ -398,12 +531,12 @@ class Loop:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not inspect.iscoroutinefunction(coro):
|
if not inspect.iscoroutinefunction(coro):
|
||||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
self._after_loop = coro
|
self._after_loop = coro
|
||||||
return coro
|
return coro
|
||||||
|
|
||||||
def error(self, coro):
|
def error(self, coro: ET) -> ET:
|
||||||
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
||||||
|
|
||||||
The coroutine must take only one argument the exception raised (except ``self`` in a class context).
|
The coroutine must take only one argument the exception raised (except ``self`` in a class context).
|
||||||
@@ -424,22 +557,82 @@ class Loop:
|
|||||||
The function was not a coroutine.
|
The function was not a coroutine.
|
||||||
"""
|
"""
|
||||||
if not inspect.iscoroutinefunction(coro):
|
if not inspect.iscoroutinefunction(coro):
|
||||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
self._error = coro
|
self._error = coro # type: ignore
|
||||||
return coro
|
return coro
|
||||||
|
|
||||||
def _get_next_sleep_time(self):
|
def _get_next_sleep_time(self) -> datetime.datetime:
|
||||||
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
|
if self._sleep is not MISSING:
|
||||||
|
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
|
||||||
|
|
||||||
def change_interval(self, *, seconds=0, minutes=0, hours=0):
|
if self._time_index >= len(self._time):
|
||||||
|
self._time_index = 0
|
||||||
|
if self._current_loop == 0:
|
||||||
|
# if we're at the last index on the first iteration, we need to sleep until tomorrow
|
||||||
|
return datetime.datetime.combine(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1), self._time[0])
|
||||||
|
|
||||||
|
next_time = self._time[self._time_index]
|
||||||
|
|
||||||
|
if self._current_loop == 0:
|
||||||
|
self._time_index += 1
|
||||||
|
return datetime.datetime.combine(datetime.datetime.now(datetime.timezone.utc), next_time)
|
||||||
|
|
||||||
|
next_date = cast(datetime.datetime, self._last_iteration)
|
||||||
|
if self._time_index == 0:
|
||||||
|
# we can assume that the earliest time should be scheduled for "tomorrow"
|
||||||
|
next_date += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
self._time_index += 1
|
||||||
|
return datetime.datetime.combine(next_date, next_time)
|
||||||
|
|
||||||
|
def _prepare_time_index(self, now: Optional[datetime.datetime] = None) -> None:
|
||||||
|
# now kwarg should be a datetime.datetime representing the time "now"
|
||||||
|
# to calculate the next time index from
|
||||||
|
|
||||||
|
# pre-condition: self._time is set
|
||||||
|
time_now = (now or datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)).timetz()
|
||||||
|
for idx, time in enumerate(self._time):
|
||||||
|
if time >= time_now:
|
||||||
|
self._time_index = idx
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._time_index = 0
|
||||||
|
|
||||||
|
def _get_time_parameter(
|
||||||
|
self,
|
||||||
|
time: Union[datetime.time, Sequence[datetime.time]],
|
||||||
|
*,
|
||||||
|
dt: Type[datetime.time] = datetime.time,
|
||||||
|
utc: datetime.timezone = datetime.timezone.utc,
|
||||||
|
) -> List[datetime.time]:
|
||||||
|
if isinstance(time, dt):
|
||||||
|
ret = time if time.tzinfo is not None else time.replace(tzinfo=utc)
|
||||||
|
return [ret]
|
||||||
|
if not isinstance(time, Sequence):
|
||||||
|
raise TypeError(f'Expected datetime.time or a sequence of datetime.time for ``time``, received {type(time)!r} instead.')
|
||||||
|
if not time:
|
||||||
|
raise ValueError('time parameter must not be an empty sequence.')
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for index, t in enumerate(time):
|
||||||
|
if not isinstance(t, dt):
|
||||||
|
raise TypeError(f'Expected a sequence of {dt!r} for ``time``, received {type(t).__name__!r} at index {index} instead.')
|
||||||
|
ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc))
|
||||||
|
|
||||||
|
ret = sorted(set(ret)) # de-dupe and sort times
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def change_interval(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
seconds: float = 0,
|
||||||
|
minutes: float = 0,
|
||||||
|
hours: float = 0,
|
||||||
|
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
|
||||||
|
) -> None:
|
||||||
"""Changes the interval for the sleep time.
|
"""Changes the interval for the sleep time.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This only applies on the next loop iteration. If it is desirable for the change of interval
|
|
||||||
to be applied right away, cancel the task with :meth:`cancel`.
|
|
||||||
|
|
||||||
.. versionadded:: 1.2
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -450,23 +643,66 @@ class Loop:
|
|||||||
The number of minutes between every iteration.
|
The number of minutes between every iteration.
|
||||||
hours: :class:`float`
|
hours: :class:`float`
|
||||||
The number of hours between every iteration.
|
The number of hours between every iteration.
|
||||||
|
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
|
||||||
|
The exact times to run this loop at. Either a non-empty list or a single
|
||||||
|
value of :class:`datetime.time` should be passed.
|
||||||
|
This cannot be used in conjunction with the relative time parameters.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Duplicate times will be ignored, and only run once.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
ValueError
|
ValueError
|
||||||
An invalid value was given.
|
An invalid value was given.
|
||||||
|
TypeError
|
||||||
|
An invalid value for the ``time`` parameter was passed, or the
|
||||||
|
``time`` parameter was passed in conjunction with relative time parameters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
|
if time is MISSING:
|
||||||
if sleep < 0:
|
seconds = seconds or 0
|
||||||
raise ValueError('Total number of seconds cannot be less than zero.')
|
minutes = minutes or 0
|
||||||
|
hours = hours or 0
|
||||||
|
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
|
||||||
|
if sleep < 0:
|
||||||
|
raise ValueError('Total number of seconds cannot be less than zero.')
|
||||||
|
|
||||||
self._sleep = sleep
|
self._sleep = sleep
|
||||||
self.seconds = seconds
|
self._seconds = float(seconds)
|
||||||
self.hours = hours
|
self._hours = float(hours)
|
||||||
self.minutes = minutes
|
self._minutes = float(minutes)
|
||||||
|
self._time: List[datetime.time] = MISSING
|
||||||
|
else:
|
||||||
|
if any((seconds, minutes, hours)):
|
||||||
|
raise TypeError('Cannot mix explicit time with relative time')
|
||||||
|
self._time = self._get_time_parameter(time)
|
||||||
|
self._sleep = self._seconds = self._minutes = self._hours = MISSING
|
||||||
|
|
||||||
def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
|
if self.is_running():
|
||||||
|
if self._time is not MISSING:
|
||||||
|
# prepare the next time index starting from after the last iteration
|
||||||
|
self._prepare_time_index(now=self._last_iteration)
|
||||||
|
|
||||||
|
self._next_iteration = self._get_next_sleep_time()
|
||||||
|
if not self._handle.done():
|
||||||
|
# the loop is sleeping, recalculate based on new interval
|
||||||
|
self._handle.recalculate(self._next_iteration)
|
||||||
|
|
||||||
|
|
||||||
|
def loop(
|
||||||
|
*,
|
||||||
|
seconds: float = MISSING,
|
||||||
|
minutes: float = MISSING,
|
||||||
|
hours: float = MISSING,
|
||||||
|
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
|
||||||
|
count: Optional[int] = None,
|
||||||
|
reconnect: bool = True,
|
||||||
|
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||||
|
) -> Callable[[Union[Callable[Concatenate[Type[C], P], _coro[T]], Callable[P, _coro[T]]]], Loop[C, P, T]]:
|
||||||
"""A decorator that schedules a task in the background for you with
|
"""A decorator that schedules a task in the background for you with
|
||||||
optional reconnect logic. The decorator returns a :class:`Loop`.
|
optional reconnect logic. The decorator returns a :class:`Loop`.
|
||||||
|
|
||||||
@@ -478,6 +714,19 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
|
|||||||
The number of minutes between every iteration.
|
The number of minutes between every iteration.
|
||||||
hours: :class:`float`
|
hours: :class:`float`
|
||||||
The number of hours between every iteration.
|
The number of hours between every iteration.
|
||||||
|
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
|
||||||
|
The exact times to run this loop at. Either a non-empty list or a single
|
||||||
|
value of :class:`datetime.time` should be passed. Timezones are supported.
|
||||||
|
If no timezone is given for the times, it is assumed to represent UTC time.
|
||||||
|
|
||||||
|
This cannot be used in conjunction with the relative time parameters.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Duplicate times will be ignored, and only run once.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
count: Optional[:class:`int`]
|
count: Optional[:class:`int`]
|
||||||
The number of loops to do, ``None`` if it should be an
|
The number of loops to do, ``None`` if it should be an
|
||||||
infinite loop.
|
infinite loop.
|
||||||
@@ -485,7 +734,7 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
|
|||||||
Whether to handle errors and restart the task
|
Whether to handle errors and restart the task
|
||||||
using an exponential back-off algorithm similar to the
|
using an exponential back-off algorithm similar to the
|
||||||
one used in :meth:`discord.Client.connect`.
|
one used in :meth:`discord.Client.connect`.
|
||||||
loop: :class:`asyncio.AbstractEventLoop`
|
loop: Optional[:class:`asyncio.AbstractEventLoop`]
|
||||||
The loop to use to register the task, if not given
|
The loop to use to register the task, if not given
|
||||||
defaults to :func:`asyncio.get_event_loop`.
|
defaults to :func:`asyncio.get_event_loop`.
|
||||||
|
|
||||||
@@ -494,16 +743,18 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
|
|||||||
ValueError
|
ValueError
|
||||||
An invalid value was given.
|
An invalid value was given.
|
||||||
TypeError
|
TypeError
|
||||||
The function was not a coroutine.
|
The function was not a coroutine, an invalid value for the ``time`` parameter was passed,
|
||||||
|
or ``time`` parameter was passed in conjunction with relative time parameters.
|
||||||
"""
|
"""
|
||||||
def decorator(func):
|
def decorator(func: Union[Callable[Concatenate[Type[C], P], _coro[T]], Callable[P, _coro[T]]]) -> Loop[C, P, T]:
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'seconds': seconds,
|
'seconds': seconds,
|
||||||
'minutes': minutes,
|
'minutes': minutes,
|
||||||
'hours': hours,
|
'hours': hours,
|
||||||
'count': count,
|
'count': count,
|
||||||
|
'time': time,
|
||||||
'reconnect': reconnect,
|
'reconnect': reconnect,
|
||||||
'loop': loop
|
'loop': loop,
|
||||||
}
|
}
|
||||||
return Loop(func, **kwargs)
|
return Loop[C, P, T](func, **kwargs)
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os.path
|
from __future__ import annotations
|
||||||
|
from typing import Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
import os
|
||||||
import io
|
import io
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'File',
|
'File',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
r"""A parameter object used for :meth:`abc.Messageable.send`
|
r"""A parameter object used for :meth:`abc.Messageable.send`
|
||||||
for sending file objects.
|
for sending file objects.
|
||||||
@@ -40,7 +44,7 @@ class File:
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
fp: Union[:class:`str`, :class:`io.BufferedIOBase`]
|
fp: Union[:class:`os.PathLike`, :class:`io.BufferedIOBase`]
|
||||||
A file-like object opened in binary mode and read mode
|
A file-like object opened in binary mode and read mode
|
||||||
or a filename representing a file in the hard drive to
|
or a filename representing a file in the hard drive to
|
||||||
open.
|
open.
|
||||||
@@ -62,9 +66,18 @@ class File:
|
|||||||
|
|
||||||
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
|
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
|
||||||
|
|
||||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
if TYPE_CHECKING:
|
||||||
self.fp = fp
|
fp: io.BufferedIOBase
|
||||||
|
filename: Optional[str]
|
||||||
|
spoiler: bool
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fp: Union[str, bytes, os.PathLike, io.BufferedIOBase],
|
||||||
|
filename: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
spoiler: bool = False,
|
||||||
|
):
|
||||||
if isinstance(fp, io.IOBase):
|
if isinstance(fp, io.IOBase):
|
||||||
if not (fp.seekable() and fp.readable()):
|
if not (fp.seekable() and fp.readable()):
|
||||||
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
|
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
|
||||||
@@ -96,7 +109,7 @@ class File:
|
|||||||
|
|
||||||
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
|
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
|
||||||
|
|
||||||
def reset(self, *, seek=True):
|
def reset(self, *, seek: Union[int, bool] = True) -> None:
|
||||||
# The `seek` parameter is needed because
|
# The `seek` parameter is needed because
|
||||||
# the retry-loop is iterated over multiple times
|
# the retry-loop is iterated over multiple times
|
||||||
# starting from 0, as an implementation quirk
|
# starting from 0, as an implementation quirk
|
||||||
@@ -108,7 +121,7 @@ class File:
|
|||||||
if seek:
|
if seek:
|
||||||
self.fp.seek(self._original_pos)
|
self.fp.seek(self._original_pos)
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
self.fp.close = self._closer
|
self.fp.close = self._closer
|
||||||
if self._owner:
|
if self._owner:
|
||||||
self._closer()
|
self._closer()
|
||||||
|
|||||||
145
discord/flags.py
145
discord/flags.py
@@ -34,11 +34,13 @@ __all__ = (
|
|||||||
'PublicUserFlags',
|
'PublicUserFlags',
|
||||||
'Intents',
|
'Intents',
|
||||||
'MemberCacheFlags',
|
'MemberCacheFlags',
|
||||||
|
'ApplicationFlags',
|
||||||
)
|
)
|
||||||
|
|
||||||
FV = TypeVar('FV', bound='flag_value')
|
FV = TypeVar('FV', bound='flag_value')
|
||||||
BF = TypeVar('BF', bound='BaseFlags')
|
BF = TypeVar('BF', bound='BaseFlags')
|
||||||
|
|
||||||
|
|
||||||
class flag_value(Generic[BF]):
|
class flag_value(Generic[BF]):
|
||||||
def __init__(self, func: Callable[[Any], int]):
|
def __init__(self, func: Callable[[Any], int]):
|
||||||
self.flag = func(None)
|
self.flag = func(None)
|
||||||
@@ -63,16 +65,20 @@ class flag_value(Generic[BF]):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<flag_value flag={self.flag!r}>'
|
return f'<flag_value flag={self.flag!r}>'
|
||||||
|
|
||||||
|
|
||||||
class alias_flag_value(flag_value):
|
class alias_flag_value(flag_value):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def fill_with_flags(*, inverted: bool = False):
|
def fill_with_flags(*, inverted: bool = False):
|
||||||
def decorator(cls: Type[BF]):
|
def decorator(cls: Type[BF]):
|
||||||
|
# fmt: off
|
||||||
cls.VALID_FLAGS = {
|
cls.VALID_FLAGS = {
|
||||||
name: value.flag
|
name: value.flag
|
||||||
for name, value in cls.__dict__.items()
|
for name, value in cls.__dict__.items()
|
||||||
if isinstance(value, flag_value)
|
if isinstance(value, flag_value)
|
||||||
}
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
if inverted:
|
if inverted:
|
||||||
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
|
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||||
@@ -81,8 +87,10 @@ def fill_with_flags(*, inverted: bool = False):
|
|||||||
cls.DEFAULT_VALUE = 0
|
cls.DEFAULT_VALUE = 0
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
# n.b. flags must inherit from this and use the decorator above
|
# n.b. flags must inherit from this and use the decorator above
|
||||||
class BaseFlags:
|
class BaseFlags:
|
||||||
VALID_FLAGS: ClassVar[Dict[str, int]]
|
VALID_FLAGS: ClassVar[Dict[str, int]]
|
||||||
@@ -136,6 +144,7 @@ class BaseFlags:
|
|||||||
else:
|
else:
|
||||||
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
|
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
|
||||||
|
|
||||||
|
|
||||||
@fill_with_flags(inverted=True)
|
@fill_with_flags(inverted=True)
|
||||||
class SystemChannelFlags(BaseFlags):
|
class SystemChannelFlags(BaseFlags):
|
||||||
r"""Wraps up a Discord system channel flag value.
|
r"""Wraps up a Discord system channel flag value.
|
||||||
@@ -199,6 +208,14 @@ class SystemChannelFlags(BaseFlags):
|
|||||||
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
|
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def guild_reminder_notifications(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the system channel is used for server setup helpful tips notifications.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 4
|
||||||
|
|
||||||
|
|
||||||
@fill_with_flags()
|
@fill_with_flags()
|
||||||
class MessageFlags(BaseFlags):
|
class MessageFlags(BaseFlags):
|
||||||
@@ -262,6 +279,14 @@ class MessageFlags(BaseFlags):
|
|||||||
"""
|
"""
|
||||||
return 16
|
return 16
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def has_thread(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the source message is associated with a thread.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 32
|
||||||
|
|
||||||
@fill_with_flags()
|
@fill_with_flags()
|
||||||
class PublicUserFlags(BaseFlags):
|
class PublicUserFlags(BaseFlags):
|
||||||
r"""Wraps up the Discord User Public flags.
|
r"""Wraps up the Discord User Public flags.
|
||||||
@@ -368,6 +393,14 @@ class PublicUserFlags(BaseFlags):
|
|||||||
"""
|
"""
|
||||||
return UserFlags.verified_bot_developer.value
|
return UserFlags.verified_bot_developer.value
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def discord_certified_moderator(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the user is a Discord Certified Moderator.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return UserFlags.discord_certified_moderator.value
|
||||||
|
|
||||||
def all(self) -> List[UserFlags]:
|
def all(self) -> List[UserFlags]:
|
||||||
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
|
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
|
||||||
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
|
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
|
||||||
@@ -488,6 +521,7 @@ class Intents(BaseFlags):
|
|||||||
This also corresponds to the following attributes and classes in terms of cache:
|
This also corresponds to the following attributes and classes in terms of cache:
|
||||||
|
|
||||||
- :meth:`Client.get_all_members`
|
- :meth:`Client.get_all_members`
|
||||||
|
- :meth:`Client.get_user`
|
||||||
- :meth:`Guild.chunk`
|
- :meth:`Guild.chunk`
|
||||||
- :meth:`Guild.fetch_members`
|
- :meth:`Guild.fetch_members`
|
||||||
- :meth:`Guild.get_member`
|
- :meth:`Guild.get_member`
|
||||||
@@ -496,7 +530,7 @@ class Intents(BaseFlags):
|
|||||||
- :attr:`Member.nick`
|
- :attr:`Member.nick`
|
||||||
- :attr:`Member.premium_since`
|
- :attr:`Member.premium_since`
|
||||||
- :attr:`User.name`
|
- :attr:`User.name`
|
||||||
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
- :attr:`User.avatar`
|
||||||
- :attr:`User.discriminator`
|
- :attr:`User.discriminator`
|
||||||
|
|
||||||
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
||||||
@@ -545,6 +579,9 @@ class Intents(BaseFlags):
|
|||||||
This corresponds to the following events:
|
This corresponds to the following events:
|
||||||
|
|
||||||
- :func:`on_guild_integrations_update`
|
- :func:`on_guild_integrations_update`
|
||||||
|
- :func:`on_integration_create`
|
||||||
|
- :func:`on_integration_update`
|
||||||
|
- :func:`on_raw_integration_delete`
|
||||||
|
|
||||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||||
"""
|
"""
|
||||||
@@ -627,7 +664,6 @@ class Intents(BaseFlags):
|
|||||||
- :func:`on_message_delete` (both guilds and DMs)
|
- :func:`on_message_delete` (both guilds and DMs)
|
||||||
- :func:`on_raw_message_delete` (both guilds and DMs)
|
- :func:`on_raw_message_delete` (both guilds and DMs)
|
||||||
- :func:`on_raw_message_edit` (both guilds and DMs)
|
- :func:`on_raw_message_edit` (both guilds and DMs)
|
||||||
- :func:`on_private_channel_create`
|
|
||||||
|
|
||||||
This also corresponds to the following attributes and classes in terms of cache:
|
This also corresponds to the following attributes and classes in terms of cache:
|
||||||
|
|
||||||
@@ -682,7 +718,6 @@ class Intents(BaseFlags):
|
|||||||
- :func:`on_message_delete` (only for DMs)
|
- :func:`on_message_delete` (only for DMs)
|
||||||
- :func:`on_raw_message_delete` (only for DMs)
|
- :func:`on_raw_message_delete` (only for DMs)
|
||||||
- :func:`on_raw_message_edit` (only for DMs)
|
- :func:`on_raw_message_edit` (only for DMs)
|
||||||
- :func:`on_private_channel_create`
|
|
||||||
|
|
||||||
This also corresponds to the following attributes and classes in terms of cache:
|
This also corresponds to the following attributes and classes in terms of cache:
|
||||||
|
|
||||||
@@ -802,6 +837,7 @@ class Intents(BaseFlags):
|
|||||||
"""
|
"""
|
||||||
return 1 << 14
|
return 1 << 14
|
||||||
|
|
||||||
|
|
||||||
@fill_with_flags()
|
@fill_with_flags()
|
||||||
class MemberCacheFlags(BaseFlags):
|
class MemberCacheFlags(BaseFlags):
|
||||||
"""Controls the library's cache policy when it comes to members.
|
"""Controls the library's cache policy when it comes to members.
|
||||||
@@ -875,17 +911,6 @@ class MemberCacheFlags(BaseFlags):
|
|||||||
def _empty(self):
|
def _empty(self):
|
||||||
return self.value == self.DEFAULT_VALUE
|
return self.value == self.DEFAULT_VALUE
|
||||||
|
|
||||||
@flag_value
|
|
||||||
def online(self):
|
|
||||||
""":class:`bool`: Whether to cache members with a status.
|
|
||||||
|
|
||||||
For example, members that are part of the initial ``GUILD_CREATE``
|
|
||||||
or become online at a later point. This requires :attr:`Intents.presences`.
|
|
||||||
|
|
||||||
Members that go offline are no longer cached.
|
|
||||||
"""
|
|
||||||
return 1
|
|
||||||
|
|
||||||
@flag_value
|
@flag_value
|
||||||
def voice(self):
|
def voice(self):
|
||||||
""":class:`bool`: Whether to cache members that are in voice.
|
""":class:`bool`: Whether to cache members that are in voice.
|
||||||
@@ -894,7 +919,7 @@ class MemberCacheFlags(BaseFlags):
|
|||||||
|
|
||||||
Members that leave voice are no longer cached.
|
Members that leave voice are no longer cached.
|
||||||
"""
|
"""
|
||||||
return 2
|
return 1
|
||||||
|
|
||||||
@flag_value
|
@flag_value
|
||||||
def joined(self):
|
def joined(self):
|
||||||
@@ -905,7 +930,7 @@ class MemberCacheFlags(BaseFlags):
|
|||||||
|
|
||||||
Members that leave the guild are no longer cached.
|
Members that leave the guild are no longer cached.
|
||||||
"""
|
"""
|
||||||
return 4
|
return 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
|
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
|
||||||
@@ -926,35 +951,89 @@ class MemberCacheFlags(BaseFlags):
|
|||||||
self = cls.none()
|
self = cls.none()
|
||||||
if intents.members:
|
if intents.members:
|
||||||
self.joined = True
|
self.joined = True
|
||||||
if intents.presences:
|
|
||||||
self.online = True
|
|
||||||
if intents.voice_states:
|
if intents.voice_states:
|
||||||
self.voice = True
|
self.voice = True
|
||||||
|
|
||||||
if not self.joined and self.online and self.voice:
|
|
||||||
self.voice = False
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _verify_intents(self, intents: Intents):
|
def _verify_intents(self, intents: Intents):
|
||||||
if self.online and not intents.presences:
|
|
||||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
|
||||||
|
|
||||||
if self.voice and not intents.voice_states:
|
if self.voice and not intents.voice_states:
|
||||||
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
|
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
|
||||||
|
|
||||||
if self.joined and not intents.members:
|
if self.joined and not intents.members:
|
||||||
raise ValueError('MemberCacheFlags.joined requires Intents.members')
|
raise ValueError('MemberCacheFlags.joined requires Intents.members')
|
||||||
|
|
||||||
if not self.joined and self.voice and self.online:
|
|
||||||
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
|
|
||||||
'to properly evict members from the cache.'
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _voice_only(self):
|
def _voice_only(self):
|
||||||
return self.value == 2
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _online_only(self):
|
|
||||||
return self.value == 1
|
return self.value == 1
|
||||||
|
|
||||||
|
|
||||||
|
@fill_with_flags()
|
||||||
|
class ApplicationFlags(BaseFlags):
|
||||||
|
r"""Wraps up the Discord Application flags.
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two ApplicationFlags are equal.
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two ApplicationFlags are not equal.
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Return the flag's hash.
|
||||||
|
.. describe:: iter(x)
|
||||||
|
|
||||||
|
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||||
|
to be, for example, constructed as a dict or a list of pairs.
|
||||||
|
Note that aliases are not shown.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
value: :class:`int`
|
||||||
|
The raw value. You should query flags via the properties
|
||||||
|
rather than using this raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def gateway_presence(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
|
||||||
|
receive presence information over the gateway.
|
||||||
|
"""
|
||||||
|
return 1 << 12
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def gateway_presence_limited(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
|
||||||
|
presence information over the gateway.
|
||||||
|
"""
|
||||||
|
return 1 << 13
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def gateway_guild_members(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
|
||||||
|
receive guild members information over the gateway.
|
||||||
|
"""
|
||||||
|
return 1 << 14
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def gateway_guild_members_limited(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
|
||||||
|
guild members information over the gateway.
|
||||||
|
"""
|
||||||
|
return 1 << 15
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def verification_pending_guild_limit(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is currently pending verification
|
||||||
|
and has hit the guild limit.
|
||||||
|
"""
|
||||||
|
return 1 << 16
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def embedded(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if the application is embedded within the Discord client."""
|
||||||
|
return 1 << 17
|
||||||
|
|||||||
@@ -373,7 +373,6 @@ class DiscordWebSocket:
|
|||||||
},
|
},
|
||||||
'compress': True,
|
'compress': True,
|
||||||
'large_threshold': 250,
|
'large_threshold': 250,
|
||||||
'guild_subscriptions': self._connection.guild_subscriptions,
|
|
||||||
'v': 3
|
'v': 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,7 +599,9 @@ class DiscordWebSocket:
|
|||||||
if activity is not None:
|
if activity is not None:
|
||||||
if not isinstance(activity, BaseActivity):
|
if not isinstance(activity, BaseActivity):
|
||||||
raise InvalidArgument('activity must derive from BaseActivity.')
|
raise InvalidArgument('activity must derive from BaseActivity.')
|
||||||
activity = activity.to_dict()
|
activity = [activity.to_dict()]
|
||||||
|
else:
|
||||||
|
activity = []
|
||||||
|
|
||||||
if status == 'idle':
|
if status == 'idle':
|
||||||
since = int(time.time() * 1000)
|
since = int(time.time() * 1000)
|
||||||
@@ -608,7 +609,7 @@ class DiscordWebSocket:
|
|||||||
payload = {
|
payload = {
|
||||||
'op': self.PRESENCE,
|
'op': self.PRESENCE,
|
||||||
'd': {
|
'd': {
|
||||||
'game': activity,
|
'activities': activity,
|
||||||
'afk': afk,
|
'afk': afk,
|
||||||
'since': since,
|
'since': since,
|
||||||
'status': status
|
'status': status
|
||||||
@@ -707,12 +708,17 @@ class DiscordVoiceWebSocket:
|
|||||||
CLIENT_CONNECT = 12
|
CLIENT_CONNECT = 12
|
||||||
CLIENT_DISCONNECT = 13
|
CLIENT_DISCONNECT = 13
|
||||||
|
|
||||||
def __init__(self, socket, loop):
|
def __init__(self, socket, loop, *, hook=None):
|
||||||
self.ws = socket
|
self.ws = socket
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self._keep_alive = None
|
self._keep_alive = None
|
||||||
self._close_code = None
|
self._close_code = None
|
||||||
self.secret_key = None
|
self.secret_key = None
|
||||||
|
if hook:
|
||||||
|
self._hook = hook
|
||||||
|
|
||||||
|
async def _hook(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
async def send_as_json(self, data):
|
async def send_as_json(self, data):
|
||||||
log.debug('Sending voice websocket frame: %s.', data)
|
log.debug('Sending voice websocket frame: %s.', data)
|
||||||
@@ -746,12 +752,12 @@ class DiscordVoiceWebSocket:
|
|||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_client(cls, client, *, resume=False):
|
async def from_client(cls, client, *, resume=False, hook=None):
|
||||||
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
||||||
gateway = 'wss://' + client.endpoint + '/?v=4'
|
gateway = 'wss://' + client.endpoint + '/?v=4'
|
||||||
http = client._state.http
|
http = client._state.http
|
||||||
socket = await http.ws_connect(gateway, compress=15)
|
socket = await http.ws_connect(gateway, compress=15)
|
||||||
ws = cls(socket, loop=client.loop)
|
ws = cls(socket, loop=client.loop, hook=hook)
|
||||||
ws.gateway = gateway
|
ws.gateway = gateway
|
||||||
ws._connection = client
|
ws._connection = client
|
||||||
ws._max_heartbeat_timeout = 60.0
|
ws._max_heartbeat_timeout = 60.0
|
||||||
@@ -819,6 +825,8 @@ class DiscordVoiceWebSocket:
|
|||||||
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0))
|
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0))
|
||||||
self._keep_alive.start()
|
self._keep_alive.start()
|
||||||
|
|
||||||
|
await self._hook(self, msg)
|
||||||
|
|
||||||
async def initial_connection(self, data):
|
async def initial_connection(self, data):
|
||||||
state = self._connection
|
state = self._connection
|
||||||
state.ssrc = data['ssrc']
|
state.ssrc = data['ssrc']
|
||||||
|
|||||||
1374
discord/guild.py
1374
discord/guild.py
File diff suppressed because it is too large
Load Diff
997
discord/http.py
997
discord/http.py
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Optional, TYPE_CHECKING, overload, Type, Tuple
|
||||||
from .utils import _get_as_snowflake, get, parse_time
|
from .utils import _get_as_snowflake, get, parse_time
|
||||||
from .user import User
|
from .user import User
|
||||||
from .errors import InvalidArgument
|
from .errors import InvalidArgument
|
||||||
@@ -30,9 +33,25 @@ from .enums import try_enum, ExpireBehaviour
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'IntegrationAccount',
|
'IntegrationAccount',
|
||||||
|
'IntegrationApplication',
|
||||||
'Integration',
|
'Integration',
|
||||||
|
'StreamIntegration',
|
||||||
|
'BotIntegration',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.integration import (
|
||||||
|
IntegrationAccount as IntegrationAccountPayload,
|
||||||
|
Integration as IntegrationPayload,
|
||||||
|
StreamIntegration as StreamIntegrationPayload,
|
||||||
|
BotIntegration as BotIntegrationPayload,
|
||||||
|
IntegrationType,
|
||||||
|
IntegrationApplication as IntegrationApplicationPayload,
|
||||||
|
)
|
||||||
|
from .guild import Guild
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
|
||||||
class IntegrationAccount:
|
class IntegrationAccount:
|
||||||
"""Represents an integration account.
|
"""Represents an integration account.
|
||||||
|
|
||||||
@@ -48,12 +67,13 @@ class IntegrationAccount:
|
|||||||
|
|
||||||
__slots__ = ('id', 'name')
|
__slots__ = ('id', 'name')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, data: IntegrationAccountPayload) -> None:
|
||||||
self.id = kwargs.pop('id')
|
self.id: Optional[int] = _get_as_snowflake(data, 'id')
|
||||||
self.name = kwargs.pop('name')
|
self.name: str = data.pop('name')
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<IntegrationAccount id={self.id} name={self.name!r}>'
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)
|
|
||||||
|
|
||||||
class Integration:
|
class Integration:
|
||||||
"""Represents a guild integration.
|
"""Represents a guild integration.
|
||||||
@@ -62,6 +82,76 @@ class Integration:
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
|
id: :class:`int`
|
||||||
|
The integration ID.
|
||||||
|
name: :class:`str`
|
||||||
|
The integration name.
|
||||||
|
guild: :class:`Guild`
|
||||||
|
The guild of the integration.
|
||||||
|
type: :class:`str`
|
||||||
|
The integration type (i.e. Twitch).
|
||||||
|
enabled: :class:`bool`
|
||||||
|
Whether the integration is currently enabled.
|
||||||
|
account: :class:`IntegrationAccount`
|
||||||
|
The account linked to this integration.
|
||||||
|
user: :class:`User`
|
||||||
|
The user that added this integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'guild',
|
||||||
|
'id',
|
||||||
|
'_state',
|
||||||
|
'type',
|
||||||
|
'name',
|
||||||
|
'account',
|
||||||
|
'user',
|
||||||
|
'enabled',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, data: IntegrationPayload, guild: Guild) -> None:
|
||||||
|
self.guild = guild
|
||||||
|
self._state = guild._state
|
||||||
|
self._from_data(data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>"
|
||||||
|
|
||||||
|
def _from_data(self, data: IntegrationPayload) -> None:
|
||||||
|
self.id: int = int(data['id'])
|
||||||
|
self.type: IntegrationType = data['type']
|
||||||
|
self.name: str = data['name']
|
||||||
|
self.account: IntegrationAccount = IntegrationAccount(data['account'])
|
||||||
|
|
||||||
|
user = data.get('user')
|
||||||
|
self.user = User(state=self._state, data=user) if user else None
|
||||||
|
self.enabled: bool = data['enabled']
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Deletes the integration.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||||
|
do this.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permission to delete the integration.
|
||||||
|
HTTPException
|
||||||
|
Deleting the integration failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.delete_integration(self.guild.id, self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class StreamIntegration(Integration):
|
||||||
|
"""Represents a stream integration for Twitch or YouTube.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
The integration ID.
|
The integration ID.
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@@ -74,8 +164,6 @@ class Integration:
|
|||||||
Whether the integration is currently enabled.
|
Whether the integration is currently enabled.
|
||||||
syncing: :class:`bool`
|
syncing: :class:`bool`
|
||||||
Where the integration is currently syncing.
|
Where the integration is currently syncing.
|
||||||
role: :class:`Role`
|
|
||||||
The role which the integration uses for subscribers.
|
|
||||||
enable_emoticons: Optional[:class:`bool`]
|
enable_emoticons: Optional[:class:`bool`]
|
||||||
Whether emoticons should be synced for this integration (currently twitch only).
|
Whether emoticons should be synced for this integration (currently twitch only).
|
||||||
expire_behaviour: :class:`ExpireBehaviour`
|
expire_behaviour: :class:`ExpireBehaviour`
|
||||||
@@ -90,37 +178,49 @@ class Integration:
|
|||||||
An aware UTC datetime representing when the integration was last synced.
|
An aware UTC datetime representing when the integration was last synced.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
|
__slots__ = (
|
||||||
'syncing', 'role', 'expire_behaviour', 'expire_behavior',
|
'revoked',
|
||||||
'expire_grace_period', 'synced_at', 'user', 'account',
|
'expire_behaviour',
|
||||||
'enable_emoticons', '_role_id')
|
'expire_behavior',
|
||||||
|
'expire_grace_period',
|
||||||
|
'synced_at',
|
||||||
|
'_role_id',
|
||||||
|
'syncing',
|
||||||
|
'enable_emoticons',
|
||||||
|
'subscriber_count',
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *, data, guild):
|
def _from_data(self, data: StreamIntegrationPayload) -> None:
|
||||||
self.guild = guild
|
super()._from_data(data)
|
||||||
self._state = guild._state
|
self.revoked: bool = data['revoked']
|
||||||
self._from_data(data)
|
self.expire_behaviour: ExpireBehaviour = try_enum(ExpireBehaviour, data['expire_behavior'])
|
||||||
|
self.expire_grace_period: int = data['expire_grace_period']
|
||||||
|
self.synced_at: datetime.datetime = parse_time(data['synced_at'])
|
||||||
|
self._role_id: int = int(data['role_id'])
|
||||||
|
self.syncing: bool = data['syncing']
|
||||||
|
self.enable_emoticons: bool = data['enable_emoticons']
|
||||||
|
self.subscriber_count: int = data['subscriber_count']
|
||||||
|
|
||||||
def __repr__(self):
|
@property
|
||||||
return '<Integration id={0.id} name={0.name!r} type={0.type!r}>'.format(self)
|
def role(self) -> Optional[Role]:
|
||||||
|
"""Optional[:class:`Role`] The role which the integration uses for subscribers."""
|
||||||
|
return self.guild.get_role(self._role_id)
|
||||||
|
|
||||||
def _from_data(self, integ):
|
@overload
|
||||||
self.id = _get_as_snowflake(integ, 'id')
|
async def edit(
|
||||||
self.name = integ['name']
|
self,
|
||||||
self.type = integ['type']
|
*,
|
||||||
self.enabled = integ['enabled']
|
expire_behaviour: Optional[ExpireBehaviour] = ...,
|
||||||
self.syncing = integ['syncing']
|
expire_grace_period: Optional[int] = ...,
|
||||||
self._role_id = _get_as_snowflake(integ, 'role_id')
|
enable_emoticons: Optional[bool] = ...,
|
||||||
self.role = get(self.guild.roles, id=self._role_id)
|
) -> None:
|
||||||
self.enable_emoticons = integ.get('enable_emoticons')
|
...
|
||||||
self.expire_behaviour = try_enum(ExpireBehaviour, integ['expire_behavior'])
|
|
||||||
self.expire_behavior = self.expire_behaviour
|
|
||||||
self.expire_grace_period = integ['expire_grace_period']
|
|
||||||
self.synced_at = parse_time(integ['synced_at'])
|
|
||||||
|
|
||||||
self.user = User(state=self._state, data=integ['user'])
|
@overload
|
||||||
self.account = IntegrationAccount(**integ['account'])
|
async def edit(self, **fields) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, **fields):
|
async def edit(self, **fields) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Edits the integration.
|
Edits the integration.
|
||||||
@@ -161,9 +261,11 @@ class Integration:
|
|||||||
'expire_grace_period': expire_grace_period,
|
'expire_grace_period': expire_grace_period,
|
||||||
}
|
}
|
||||||
|
|
||||||
enable_emoticons = fields.get('enable_emoticons')
|
try:
|
||||||
|
enable_emoticons = fields['enable_emoticons']
|
||||||
if enable_emoticons is not None:
|
except KeyError:
|
||||||
|
enable_emoticons = self.enable_emoticons
|
||||||
|
else:
|
||||||
payload['enable_emoticons'] = enable_emoticons
|
payload['enable_emoticons'] = enable_emoticons
|
||||||
|
|
||||||
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
|
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
|
||||||
@@ -173,7 +275,7 @@ class Integration:
|
|||||||
self.expire_grace_period = expire_grace_period
|
self.expire_grace_period = expire_grace_period
|
||||||
self.enable_emoticons = enable_emoticons
|
self.enable_emoticons = enable_emoticons
|
||||||
|
|
||||||
async def sync(self):
|
async def sync(self) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Syncs the integration.
|
Syncs the integration.
|
||||||
@@ -191,19 +293,83 @@ class Integration:
|
|||||||
await self._state.http.sync_integration(self.guild.id, self.id)
|
await self._state.http.sync_integration(self.guild.id, self.id)
|
||||||
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
|
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
async def delete(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Deletes the integration.
|
class IntegrationApplication:
|
||||||
|
"""Represents an application for a bot integration.
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
.. versionadded:: 2.0
|
||||||
do this.
|
|
||||||
|
|
||||||
Raises
|
Attributes
|
||||||
-------
|
----------
|
||||||
Forbidden
|
id: :class:`int`
|
||||||
You do not have permission to delete the integration.
|
The ID for this application.
|
||||||
HTTPException
|
name: :class:`str`
|
||||||
Deleting the integration failed.
|
The application's name.
|
||||||
"""
|
icon: Optional[:class:`str`]
|
||||||
await self._state.http.delete_integration(self.guild.id, self.id)
|
The application's icon hash.
|
||||||
|
description: :class:`str`
|
||||||
|
The application's description. Can be an empty string.
|
||||||
|
summary: :class:`str`
|
||||||
|
The summary of the application. Can be an empty string.
|
||||||
|
user: Optional[:class:`User`]
|
||||||
|
The bot user on this application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'icon',
|
||||||
|
'description',
|
||||||
|
'summary',
|
||||||
|
'user',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, data: IntegrationApplicationPayload, state):
|
||||||
|
self.id: int = int(data['id'])
|
||||||
|
self.name: str = data['name']
|
||||||
|
self.icon: Optional[str] = data['icon']
|
||||||
|
self.description: str = data['description']
|
||||||
|
self.summary: str = data['summary']
|
||||||
|
user = data.get('bot')
|
||||||
|
self.user: Optional[User] = User(state=state, data=user) if user else None
|
||||||
|
|
||||||
|
|
||||||
|
class BotIntegration(Integration):
|
||||||
|
"""Represents a bot integration on discord.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
id: :class:`int`
|
||||||
|
The integration ID.
|
||||||
|
name: :class:`str`
|
||||||
|
The integration name.
|
||||||
|
guild: :class:`Guild`
|
||||||
|
The guild of the integration.
|
||||||
|
type: :class:`str`
|
||||||
|
The integration type (i.e. Twitch).
|
||||||
|
enabled: :class:`bool`
|
||||||
|
Whether the integration is currently enabled.
|
||||||
|
user: :class:`User`
|
||||||
|
The user that added this integration.
|
||||||
|
account: :class:`IntegrationAccount`
|
||||||
|
The integration account information.
|
||||||
|
application: :class:`IntegrationApplication`
|
||||||
|
The application tied to this integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('application',)
|
||||||
|
|
||||||
|
def _from_data(self, data: BotIntegrationPayload) -> None:
|
||||||
|
super()._from_data(data)
|
||||||
|
self.application = IntegrationApplication(data=data['application'], state=self._state)
|
||||||
|
|
||||||
|
|
||||||
|
def _integration_factory(value: str) -> Tuple[Type[Integration], str]:
|
||||||
|
if value == 'discord':
|
||||||
|
return BotIntegration, value
|
||||||
|
elif value in ('twitch', 'youtube'):
|
||||||
|
return StreamIntegration, value
|
||||||
|
else:
|
||||||
|
return Integration, value
|
||||||
|
|||||||
@@ -25,14 +25,37 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from discord.types.interactions import InteractionResponse
|
||||||
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .enums import try_enum, InteractionType
|
from .enums import try_enum, InteractionType, InteractionResponseType
|
||||||
|
|
||||||
|
from .user import User
|
||||||
|
from .member import Member
|
||||||
|
from .message import Message, Attachment
|
||||||
|
from .object import Object
|
||||||
|
from .webhook.async_ import async_context, Webhook
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Interaction',
|
'Interaction',
|
||||||
|
'InteractionResponse',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.interactions import (
|
||||||
|
Interaction as InteractionPayload,
|
||||||
|
)
|
||||||
|
from .guild import Guild
|
||||||
|
from .abc import GuildChannel
|
||||||
|
from .state import ConnectionState
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from .embeds import Embed
|
||||||
|
from .ui.view import View
|
||||||
|
|
||||||
|
MISSING: Any = utils.MISSING
|
||||||
|
|
||||||
|
|
||||||
class Interaction:
|
class Interaction:
|
||||||
"""Represents a Discord interaction.
|
"""Represents a Discord interaction.
|
||||||
|
|
||||||
@@ -56,28 +79,36 @@ class Interaction:
|
|||||||
The application ID that the interaction was for.
|
The application ID that the interaction was for.
|
||||||
user: Optional[Union[:class:`User`, :class:`Member`]]
|
user: Optional[Union[:class:`User`, :class:`Member`]]
|
||||||
The user or member that sent the interaction.
|
The user or member that sent the interaction.
|
||||||
|
message: Optional[:class:`Message`]
|
||||||
|
The message that sent this interaction.
|
||||||
token: :class:`str`
|
token: :class:`str`
|
||||||
The token to continue the interaction. These are valid
|
The token to continue the interaction. These are valid
|
||||||
for 15 minutes.
|
for 15 minutes.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
'id',
|
'id',
|
||||||
'type',
|
'type',
|
||||||
'guild_id',
|
'guild_id',
|
||||||
'channel_id',
|
'channel_id',
|
||||||
'data',
|
'data',
|
||||||
'application_id',
|
'application_id',
|
||||||
|
'message',
|
||||||
'user',
|
'user',
|
||||||
'token',
|
'token',
|
||||||
'version',
|
'version',
|
||||||
'_state',
|
'_state',
|
||||||
|
'_session',
|
||||||
|
'_cs_response',
|
||||||
|
'_cs_followup',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *, data, state=None):
|
def __init__(self, *, data: InteractionPayload, state: ConnectionState):
|
||||||
self._state = state
|
self._state = state
|
||||||
|
self._session: ClientSession = state.http._HTTPClient__session
|
||||||
self._from_data(data)
|
self._from_data(data)
|
||||||
|
|
||||||
def _from_data(self, data):
|
def _from_data(self, data: InteractionPayload):
|
||||||
self.id = int(data['id'])
|
self.id = int(data['id'])
|
||||||
self.type = try_enum(InteractionType, data['type'])
|
self.type = try_enum(InteractionType, data['type'])
|
||||||
self.data = data.get('data')
|
self.data = data.get('data')
|
||||||
@@ -87,13 +118,34 @@ class Interaction:
|
|||||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||||
self.application_id = utils._get_as_snowflake(data, 'application_id')
|
self.application_id = utils._get_as_snowflake(data, 'application_id')
|
||||||
|
|
||||||
@property
|
channel = self.channel or Object(id=self.channel_id)
|
||||||
def guild(self):
|
try:
|
||||||
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
|
self.message = Message(state=self._state, channel=channel, data=data['message'])
|
||||||
return self._state and self._state.get_guild(self.guild_id)
|
except KeyError:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
self.user: Optional[Union[User, Member]] = None
|
||||||
|
|
||||||
|
# TODO: there's a potential data loss here
|
||||||
|
if self.guild_id:
|
||||||
|
guild = self.guild or Object(id=self.guild_id)
|
||||||
|
try:
|
||||||
|
self.user = Member(state=self._state, guild=guild, data=data['member'])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.user = User(state=self._state, data=data['user'])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel(self):
|
def guild(self) -> Optional[Guild]:
|
||||||
|
"""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[GuildChannel]:
|
||||||
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
|
"""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
|
Note that due to a Discord limitation, DM channels are not resolved since there is
|
||||||
@@ -102,3 +154,275 @@ class Interaction:
|
|||||||
guild = self.guild
|
guild = self.guild
|
||||||
return guild and guild.get_channel(self.channel_id)
|
return guild and guild.get_channel(self.channel_id)
|
||||||
|
|
||||||
|
@utils.cached_slot_property('_cs_response')
|
||||||
|
def response(self) -> InteractionResponse:
|
||||||
|
""":class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction."""
|
||||||
|
return InteractionResponse(self)
|
||||||
|
|
||||||
|
@utils.cached_slot_property('_cs_followup')
|
||||||
|
def followup(self) -> Webhook:
|
||||||
|
""":class:`Webhook`: Returns the follow up webhook for follow up interactions."""
|
||||||
|
payload = {
|
||||||
|
'id': self.application_id,
|
||||||
|
'type': 3,
|
||||||
|
'token': self.token,
|
||||||
|
}
|
||||||
|
return Webhook.from_state(data=payload, state=self._state)
|
||||||
|
|
||||||
|
|
||||||
|
class InteractionResponse:
|
||||||
|
"""Represents a Discord interaction response.
|
||||||
|
|
||||||
|
This type can be accessed through :attr:`Interaction.response`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__: Tuple[str, ...] = (
|
||||||
|
'_responded',
|
||||||
|
'_parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, parent: Interaction):
|
||||||
|
self._parent: Interaction = parent
|
||||||
|
self._responded: bool = False
|
||||||
|
|
||||||
|
async def defer(self, *, ephemeral: bool = False) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Defers the interaction response.
|
||||||
|
|
||||||
|
This is typically used when the interaction is acknowledged
|
||||||
|
and a secondary action will be done later.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
ephemeral: :class:`bool`
|
||||||
|
Indicates whether the deferred message will eventually be ephemeral.
|
||||||
|
This only applies for interactions of type :attr:`InteractionType.application_command`.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Deferring the interaction failed.
|
||||||
|
"""
|
||||||
|
if self._responded:
|
||||||
|
return
|
||||||
|
|
||||||
|
defer_type: int = 0
|
||||||
|
data: Optional[Dict[str, Any]] = None
|
||||||
|
parent = self._parent
|
||||||
|
if parent.type is InteractionType.component:
|
||||||
|
defer_type = InteractionResponseType.deferred_message_update.value
|
||||||
|
elif parent.type is InteractionType.application_command:
|
||||||
|
defer_type = InteractionResponseType.deferred_channel_message.value
|
||||||
|
if ephemeral:
|
||||||
|
data = {'flags': 64}
|
||||||
|
|
||||||
|
if defer_type:
|
||||||
|
adapter = async_context.get()
|
||||||
|
await adapter.create_interaction_response(
|
||||||
|
parent.id, parent.token, session=parent._session, type=defer_type, data=data
|
||||||
|
)
|
||||||
|
self._responded = True
|
||||||
|
|
||||||
|
async def pong(self) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Pongs the ping interaction.
|
||||||
|
|
||||||
|
This should rarely be used.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Ponging the interaction failed.
|
||||||
|
"""
|
||||||
|
if self._responded:
|
||||||
|
return
|
||||||
|
|
||||||
|
parent = self._parent
|
||||||
|
if parent.type is InteractionType.ping:
|
||||||
|
adapter = async_context.get()
|
||||||
|
await adapter.create_interaction_response(
|
||||||
|
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
|
||||||
|
)
|
||||||
|
self._responded = True
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self,
|
||||||
|
content: Optional[Any] = None,
|
||||||
|
*,
|
||||||
|
embed: Embed = MISSING,
|
||||||
|
embeds: List[Embed] = MISSING,
|
||||||
|
view: View = MISSING,
|
||||||
|
tts: bool = False,
|
||||||
|
ephemeral: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Responds to this interaction by sending a message.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
content: Optional[:class:`str`]
|
||||||
|
The content of the message to send.
|
||||||
|
embeds: List[:class:`Embed`]
|
||||||
|
A list of embeds to send with the content. Maximum of 10. This cannot
|
||||||
|
be mixed with the ``embed`` parameter.
|
||||||
|
embed: :class:`Embed`
|
||||||
|
The rich embed for the content to send. This cannot be mixed with
|
||||||
|
``embeds`` parameter.
|
||||||
|
tts: :class:`bool`
|
||||||
|
Indicates if the message should be sent using text-to-speech.
|
||||||
|
view: :class:`discord.ui.View`
|
||||||
|
The view to send with the message.
|
||||||
|
ephemeral: :class:`bool`
|
||||||
|
Indicates if the message should only be visible to the user who started the interaction.
|
||||||
|
If a view is sent with an ephemeral message and it has no timeout set then the timeout
|
||||||
|
is set to 15 minutes.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Sending the message failed.
|
||||||
|
TypeError
|
||||||
|
You specified both ``embed`` and ``embeds``.
|
||||||
|
ValueError
|
||||||
|
The length of ``embeds`` was invalid.
|
||||||
|
"""
|
||||||
|
if self._responded:
|
||||||
|
return
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
'tts': tts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if embed is not MISSING and embeds is not MISSING:
|
||||||
|
raise TypeError('cannot mix embed and embeds keyword arguments')
|
||||||
|
|
||||||
|
if embed is not MISSING:
|
||||||
|
embeds = [embed]
|
||||||
|
|
||||||
|
if embeds:
|
||||||
|
if len(embeds) > 10:
|
||||||
|
raise ValueError('embeds cannot exceed maximum of 10 elements')
|
||||||
|
payload['embeds'] = [e.to_dict() for e in embeds]
|
||||||
|
|
||||||
|
if content is not None:
|
||||||
|
payload['content'] = str(content)
|
||||||
|
|
||||||
|
if ephemeral:
|
||||||
|
payload['flags'] = 64
|
||||||
|
|
||||||
|
if view is not MISSING:
|
||||||
|
payload['components'] = view.to_components()
|
||||||
|
|
||||||
|
parent = self._parent
|
||||||
|
adapter = async_context.get()
|
||||||
|
await adapter.create_interaction_response(
|
||||||
|
parent.id,
|
||||||
|
parent.token,
|
||||||
|
session=parent._session,
|
||||||
|
type=InteractionResponseType.channel_message.value,
|
||||||
|
data=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if view is not MISSING:
|
||||||
|
if ephemeral and view.timeout is None:
|
||||||
|
view.timeout = 15 * 60.0
|
||||||
|
|
||||||
|
self._parent._state.store_view(view)
|
||||||
|
|
||||||
|
self._responded = True
|
||||||
|
|
||||||
|
async def edit_message(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
content: Optional[Any] = MISSING,
|
||||||
|
embed: Optional[Embed] = MISSING,
|
||||||
|
embeds: List[Embed] = MISSING,
|
||||||
|
attachments: List[Attachment] = MISSING,
|
||||||
|
view: Optional[View] = MISSING,
|
||||||
|
) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Responds to this interaction by editing the original message of
|
||||||
|
a component interaction.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
content: Optional[:class:`str`]
|
||||||
|
The new content to replace the message with. ``None`` removes the content.
|
||||||
|
embeds: List[:class:`Embed`]
|
||||||
|
A list of embeds to edit the message with.
|
||||||
|
embed: Optional[:class:`Embed`]
|
||||||
|
The embed to edit the message with. ``None`` suppresses the embeds.
|
||||||
|
This should not be mixed with the ``embeds`` parameter.
|
||||||
|
attachments: List[:class:`Attachment`]
|
||||||
|
A list of attachments to keep in the message. If ``[]`` is passed
|
||||||
|
then all attachments are removed.
|
||||||
|
view: Optional[:class:`~discord.ui.View`]
|
||||||
|
The updated view to update this message with. If ``None`` is passed then
|
||||||
|
the view is removed.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Editing the message failed.
|
||||||
|
TypeError
|
||||||
|
You specified both ``embed`` and ``embeds``.
|
||||||
|
"""
|
||||||
|
if self._responded:
|
||||||
|
return
|
||||||
|
|
||||||
|
parent = self._parent
|
||||||
|
msg = parent.message
|
||||||
|
state = parent._state
|
||||||
|
message_id = msg.id if msg else None
|
||||||
|
if parent.type is not InteractionType.component:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: embeds: List[Embed]?
|
||||||
|
payload = {}
|
||||||
|
if content is not MISSING:
|
||||||
|
if content is None:
|
||||||
|
payload['content'] = None
|
||||||
|
else:
|
||||||
|
payload['content'] = str(content)
|
||||||
|
|
||||||
|
if embed is not MISSING and embeds is not MISSING:
|
||||||
|
raise TypeError('cannot mix both embed and embeds keyword arguments')
|
||||||
|
|
||||||
|
if embed is not MISSING:
|
||||||
|
if embed is None:
|
||||||
|
embeds = []
|
||||||
|
else:
|
||||||
|
embeds = [embed]
|
||||||
|
|
||||||
|
if embeds is not MISSING:
|
||||||
|
payload['embeds'] = [e.to_dict() for e in embeds]
|
||||||
|
|
||||||
|
if attachments is not MISSING:
|
||||||
|
payload['attachments'] = [a.to_dict() for a in attachments]
|
||||||
|
|
||||||
|
if view is not MISSING:
|
||||||
|
state.prevent_view_updates_for(message_id)
|
||||||
|
if view is None:
|
||||||
|
payload['components'] = []
|
||||||
|
else:
|
||||||
|
payload['components'] = view.to_components()
|
||||||
|
|
||||||
|
adapter = async_context.get()
|
||||||
|
await adapter.create_interaction_response(
|
||||||
|
parent.id,
|
||||||
|
parent.token,
|
||||||
|
session=parent._session,
|
||||||
|
type=InteractionResponseType.message_update.value,
|
||||||
|
data=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if view and not view.is_finished():
|
||||||
|
state.store_view(view, message_id)
|
||||||
|
|
||||||
|
self._responded = True
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Type, TypeVar, Union, TYPE_CHECKING
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .utils import parse_time, snowflake_time, _get_as_snowflake
|
from .utils import parse_time, snowflake_time, _get_as_snowflake
|
||||||
from .object import Object
|
from .object import Object
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
from .enums import ChannelType, VerificationLevel, try_enum
|
from .enums import ChannelType, VerificationLevel, InviteTarget, try_enum
|
||||||
|
from .appinfo import PartialAppInfo
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PartialInviteChannel',
|
'PartialInviteChannel',
|
||||||
@@ -34,6 +38,26 @@ __all__ = (
|
|||||||
'Invite',
|
'Invite',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.invite import (
|
||||||
|
Invite as InvitePayload,
|
||||||
|
InviteGuild as InviteGuildPayload,
|
||||||
|
GatewayInvite as GatewayInvitePayload,
|
||||||
|
)
|
||||||
|
from .types.channel import (
|
||||||
|
PartialChannel as InviteChannelPayload,
|
||||||
|
)
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .guild import Guild
|
||||||
|
from .abc import GuildChannel
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
InviteGuildType = Union[Guild, 'PartialInviteGuild', Object]
|
||||||
|
InviteChannelType = Union[GuildChannel, 'PartialInviteChannel', Object]
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class PartialInviteChannel:
|
class PartialInviteChannel:
|
||||||
"""Represents a "partial" invite channel.
|
"""Represents a "partial" invite channel.
|
||||||
|
|
||||||
@@ -70,27 +94,28 @@ class PartialInviteChannel:
|
|||||||
|
|
||||||
__slots__ = ('id', 'name', 'type')
|
__slots__ = ('id', 'name', 'type')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, data: InviteChannelPayload):
|
||||||
self.id = kwargs.pop('id')
|
self.id: int = int(data['id'])
|
||||||
self.name = kwargs.pop('name')
|
self.name: str = data['name']
|
||||||
self.type = kwargs.pop('type')
|
self.type: ChannelType = try_enum(ChannelType, data['type'])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
|
return f'<PartialInviteChannel id={self.id} name={self.name} type={self.type!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mention(self):
|
def mention(self) -> str:
|
||||||
""":class:`str`: The string that allows you to mention the channel."""
|
""":class:`str`: The string that allows you to mention the channel."""
|
||||||
return f'<#{self.id}>'
|
return f'<#{self.id}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
|
|
||||||
class PartialInviteGuild:
|
class PartialInviteGuild:
|
||||||
"""Represents a "partial" invite guild.
|
"""Represents a "partial" invite guild.
|
||||||
|
|
||||||
@@ -125,93 +150,61 @@ class PartialInviteGuild:
|
|||||||
The partial guild's verification level.
|
The partial guild's verification level.
|
||||||
features: List[:class:`str`]
|
features: List[:class:`str`]
|
||||||
A list of features the guild has. See :attr:`Guild.features` for more information.
|
A list of features the guild has. See :attr:`Guild.features` for more information.
|
||||||
icon: Optional[:class:`str`]
|
|
||||||
The partial guild's icon.
|
|
||||||
banner: Optional[:class:`str`]
|
|
||||||
The partial guild's banner.
|
|
||||||
splash: Optional[:class:`str`]
|
|
||||||
The partial guild's invite splash.
|
|
||||||
description: Optional[:class:`str`]
|
description: Optional[:class:`str`]
|
||||||
The partial guild's description.
|
The partial guild's description.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash',
|
__slots__ = ('_state', 'features', '_icon', '_banner', 'id', 'name', '_splash', 'verification_level', 'description')
|
||||||
'verification_level', 'description')
|
|
||||||
|
|
||||||
def __init__(self, state, data, id):
|
def __init__(self, state: ConnectionState, data: InviteGuildPayload, id: int):
|
||||||
self._state = state
|
self._state: ConnectionState = state
|
||||||
self.id = id
|
self.id: int = id
|
||||||
self.name = data['name']
|
self.name: str = data['name']
|
||||||
self.features = data.get('features', [])
|
self.features: List[str] = data.get('features', [])
|
||||||
self.icon = data.get('icon')
|
self._icon: Optional[str] = data.get('icon')
|
||||||
self.banner = data.get('banner')
|
self._banner: Optional[str] = data.get('banner')
|
||||||
self.splash = data.get('splash')
|
self._splash: Optional[str] = data.get('splash')
|
||||||
self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
|
self.verification_level: VerificationLevel = try_enum(VerificationLevel, data.get('verification_level'))
|
||||||
self.description = data.get('description')
|
self.description: Optional[str] = data.get('description')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} features={0.features} ' \
|
return (
|
||||||
'description={0.description!r}>'.format(self)
|
f'<{self.__class__.__name__} id={self.id} name={self.name!r} features={self.features} '
|
||||||
|
f'description={self.description!r}>'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon_url(self):
|
def icon(self) -> Optional[Asset]:
|
||||||
""":class:`Asset`: Returns the guild's icon asset."""
|
"""Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
|
||||||
return self.icon_url_as()
|
if self._icon is None:
|
||||||
|
return None
|
||||||
def is_icon_animated(self):
|
return Asset._from_guild_icon(self._state, self.id, self._icon)
|
||||||
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
|
|
||||||
|
|
||||||
.. versionadded:: 1.4
|
|
||||||
"""
|
|
||||||
return bool(self.icon and self.icon.startswith('a_'))
|
|
||||||
|
|
||||||
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
|
|
||||||
"""The same operation as :meth:`Guild.icon_url_as`.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def banner_url(self):
|
def banner(self) -> Optional[Asset]:
|
||||||
""":class:`Asset`: Returns the guild's banner asset."""
|
"""Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
|
||||||
return self.banner_url_as()
|
if self._banner is None:
|
||||||
|
return None
|
||||||
def banner_url_as(self, *, format='webp', size=2048):
|
return Asset._from_guild_image(self._state, self.id, self._banner, path='banners')
|
||||||
"""The same operation as :meth:`Guild.banner_url_as`.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def splash_url(self):
|
def splash(self) -> Optional[Asset]:
|
||||||
""":class:`Asset`: Returns the guild's invite splash asset."""
|
"""Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
|
||||||
return self.splash_url_as()
|
if self._splash is None:
|
||||||
|
return None
|
||||||
|
return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
|
||||||
|
|
||||||
def splash_url_as(self, *, format='webp', size=2048):
|
|
||||||
"""The same operation as :meth:`Guild.splash_url_as`.
|
|
||||||
|
|
||||||
Returns
|
I = TypeVar('I', bound='Invite')
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
|
|
||||||
|
|
||||||
class Invite(Hashable):
|
class Invite(Hashable):
|
||||||
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
||||||
@@ -239,23 +232,25 @@ class Invite(Hashable):
|
|||||||
|
|
||||||
The following table illustrates what methods will obtain the attributes:
|
The following table illustrates what methods will obtain the attributes:
|
||||||
|
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| Attribute | Method |
|
| Attribute | Method |
|
||||||
+====================================+==========================================================+
|
+====================================+============================================================+
|
||||||
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` |
|
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` |
|
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
|
||||||
+------------------------------------+----------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
|
| :attr:`expires_at` | :meth:`Client.fetch_invite` with `with_expiration` enabled |
|
||||||
|
+------------------------------------+------------------------------------------------------------+
|
||||||
|
|
||||||
If it's not in the table above then it is available by all methods.
|
If it's not in the table above then it is available by all methods.
|
||||||
|
|
||||||
@@ -280,105 +275,185 @@ class Invite(Hashable):
|
|||||||
max_uses: :class:`int`
|
max_uses: :class:`int`
|
||||||
How many times the invite can be used.
|
How many times the invite can be used.
|
||||||
A value of ``0`` indicates that it has unlimited uses.
|
A value of ``0`` indicates that it has unlimited uses.
|
||||||
inviter: :class:`User`
|
inviter: Optional[:class:`User`]
|
||||||
The user who created the invite.
|
The user who created the invite.
|
||||||
approximate_member_count: Optional[:class:`int`]
|
approximate_member_count: Optional[:class:`int`]
|
||||||
The approximate number of members in the guild.
|
The approximate number of members in the guild.
|
||||||
approximate_presence_count: Optional[:class:`int`]
|
approximate_presence_count: Optional[:class:`int`]
|
||||||
The approximate number of members currently active in the guild.
|
The approximate number of members currently active in the guild.
|
||||||
This includes idle, dnd, online, and invisible members. Offline members are excluded.
|
This includes idle, dnd, online, and invisible members. Offline members are excluded.
|
||||||
|
expires_at: Optional[:class:`datetime.datetime`]
|
||||||
|
The expiration date of the invite. If the value is ``None`` when received through
|
||||||
|
`Client.fetch_invite` with `with_expiration` enabled, the invite will never expire.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]
|
channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]
|
||||||
The channel the invite is for.
|
The channel the invite is for.
|
||||||
|
target_type: :class:`InviteTarget`
|
||||||
|
The type of target for the voice channel invite.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
target_user: Optional[:class:`User`]
|
||||||
|
The user whose stream to display for this invite, if any.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
target_application: Optional[:class:`PartialAppInfo`]
|
||||||
|
The embedded application the invite targets, if any.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses',
|
__slots__ = (
|
||||||
'temporary', 'max_uses', 'inviter', 'channel', '_state',
|
'max_age',
|
||||||
'approximate_member_count', 'approximate_presence_count' )
|
'code',
|
||||||
|
'guild',
|
||||||
|
'revoked',
|
||||||
|
'created_at',
|
||||||
|
'uses',
|
||||||
|
'temporary',
|
||||||
|
'max_uses',
|
||||||
|
'inviter',
|
||||||
|
'channel',
|
||||||
|
'target_user',
|
||||||
|
'target_type',
|
||||||
|
'_state',
|
||||||
|
'approximate_member_count',
|
||||||
|
'approximate_presence_count',
|
||||||
|
'target_application',
|
||||||
|
'expires_at',
|
||||||
|
)
|
||||||
|
|
||||||
BASE = 'https://discord.gg'
|
BASE = 'https://discord.gg'
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
def __init__(
|
||||||
self._state = state
|
self,
|
||||||
self.max_age = data.get('max_age')
|
*,
|
||||||
self.code = data.get('code')
|
state: ConnectionState,
|
||||||
self.guild = data.get('guild')
|
data: InvitePayload,
|
||||||
self.revoked = data.get('revoked')
|
guild: Optional[Union[PartialInviteGuild, Guild]] = None,
|
||||||
self.created_at = parse_time(data.get('created_at'))
|
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
|
||||||
self.temporary = data.get('temporary')
|
):
|
||||||
self.uses = data.get('uses')
|
self._state: ConnectionState = state
|
||||||
self.max_uses = data.get('max_uses')
|
self.max_age: Optional[int] = data.get('max_age')
|
||||||
self.approximate_presence_count = data.get('approximate_presence_count')
|
self.code: str = data['code']
|
||||||
self.approximate_member_count = data.get('approximate_member_count')
|
self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get('guild'), guild)
|
||||||
|
self.revoked: Optional[bool] = data.get('revoked')
|
||||||
|
self.created_at: Optional[datetime.datetime] = parse_time(data.get('created_at'))
|
||||||
|
self.temporary: Optional[bool] = data.get('temporary')
|
||||||
|
self.uses: Optional[int] = data.get('uses')
|
||||||
|
self.max_uses: Optional[int] = data.get('max_uses')
|
||||||
|
self.approximate_presence_count: Optional[int] = data.get('approximate_presence_count')
|
||||||
|
self.approximate_member_count: Optional[int] = data.get('approximate_member_count')
|
||||||
|
|
||||||
|
expires_at = data.get('expires_at', None)
|
||||||
|
self.expires_at: Optional[datetime.datetime] = parse_time(expires_at) if expires_at else None
|
||||||
|
|
||||||
inviter_data = data.get('inviter')
|
inviter_data = data.get('inviter')
|
||||||
self.inviter = None if inviter_data is None else self._state.store_user(inviter_data)
|
self.inviter: Optional[User] = None if inviter_data is None else self._state.store_user(inviter_data)
|
||||||
self.channel = data.get('channel')
|
|
||||||
|
self.channel: Optional[InviteChannelType] = self._resolve_channel(data.get('channel'), channel)
|
||||||
|
|
||||||
|
target_user_data = data.get('target_user')
|
||||||
|
self.target_user: Optional[User] = None if target_user_data is None else self._state.store_user(target_user_data)
|
||||||
|
|
||||||
|
self.target_type: InviteTarget = try_enum(InviteTarget, data.get("target_type", 0))
|
||||||
|
|
||||||
|
application = data.get('target_application')
|
||||||
|
self.target_application: Optional[PartialAppInfo] = (
|
||||||
|
PartialAppInfo(data=application, state=state) if application else None
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_incomplete(cls, *, state, data):
|
def from_incomplete(cls: Type[I], *, state: ConnectionState, data: InvitePayload) -> I:
|
||||||
|
guild: Optional[Union[Guild, PartialInviteGuild]]
|
||||||
try:
|
try:
|
||||||
guild_id = int(data['guild']['id'])
|
guild_data = data['guild']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# If we're here, then this is a group DM
|
# If we're here, then this is a group DM
|
||||||
guild = None
|
guild = None
|
||||||
else:
|
else:
|
||||||
|
guild_id = int(guild_data['id'])
|
||||||
guild = state._get_guild(guild_id)
|
guild = state._get_guild(guild_id)
|
||||||
if guild is None:
|
if guild is None:
|
||||||
# If it's not cached, then it has to be a partial guild
|
# If it's not cached, then it has to be a partial guild
|
||||||
guild_data = data['guild']
|
|
||||||
guild = PartialInviteGuild(state, guild_data, guild_id)
|
guild = PartialInviteGuild(state, guild_data, guild_id)
|
||||||
|
|
||||||
# As far as I know, invites always need a channel
|
# As far as I know, invites always need a channel
|
||||||
# So this should never raise.
|
# So this should never raise.
|
||||||
channel_data = data['channel']
|
channel: Union[PartialInviteChannel, GuildChannel] = PartialInviteChannel(data['channel'])
|
||||||
channel_id = int(channel_data['id'])
|
|
||||||
channel_type = try_enum(ChannelType, channel_data['type'])
|
|
||||||
channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type)
|
|
||||||
if guild is not None and not isinstance(guild, PartialInviteGuild):
|
if guild is not None and not isinstance(guild, PartialInviteGuild):
|
||||||
# Upgrade the partial data if applicable
|
# Upgrade the partial data if applicable
|
||||||
channel = guild.get_channel(channel_id) or channel
|
channel = guild.get_channel(channel.id) or channel
|
||||||
|
|
||||||
data['guild'] = guild
|
return cls(state=state, data=data, guild=guild, channel=channel)
|
||||||
data['channel'] = channel
|
|
||||||
return cls(state=state, data=data)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_gateway(cls, *, state, data):
|
def from_gateway(cls: Type[I], *, state: ConnectionState, data: GatewayInvitePayload) -> I:
|
||||||
guild_id = _get_as_snowflake(data, 'guild_id')
|
guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
|
||||||
guild = state._get_guild(guild_id)
|
guild: Optional[Union[Guild, Object]] = state._get_guild(guild_id)
|
||||||
channel_id = _get_as_snowflake(data, 'channel_id')
|
channel_id = int(data['channel_id'])
|
||||||
if guild is not None:
|
if guild is not None:
|
||||||
channel = guild.get_channel(channel_id) or Object(id=channel_id)
|
channel = guild.get_channel(channel_id) or Object(id=channel_id) # type: ignore
|
||||||
else:
|
else:
|
||||||
guild = Object(id=guild_id)
|
guild = Object(id=guild_id) if guild_id is not None else None
|
||||||
channel = Object(id=channel_id)
|
channel = Object(id=channel_id)
|
||||||
|
|
||||||
data['guild'] = guild
|
return cls(state=state, data=data, guild=guild, channel=channel) # type: ignore
|
||||||
data['channel'] = channel
|
|
||||||
return cls(state=state, data=data)
|
|
||||||
|
|
||||||
def __str__(self):
|
def _resolve_guild(
|
||||||
|
self,
|
||||||
|
data: Optional[InviteGuildPayload],
|
||||||
|
guild: Optional[Union[Guild, PartialInviteGuild]] = None,
|
||||||
|
) -> Optional[InviteGuildType]:
|
||||||
|
if guild is not None:
|
||||||
|
return guild
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
guild_id = int(data['id'])
|
||||||
|
return PartialInviteGuild(self._state, data, guild_id)
|
||||||
|
|
||||||
|
def _resolve_channel(
|
||||||
|
self,
|
||||||
|
data: Optional[InviteChannelPayload],
|
||||||
|
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
|
||||||
|
) -> Optional[InviteChannelType]:
|
||||||
|
if channel is not None:
|
||||||
|
return channel
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return PartialInviteChannel(data)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Invite code={0.code!r} guild={0.guild!r} ' \
|
return (
|
||||||
'online={0.approximate_presence_count} ' \
|
f'<Invite code={self.code!r} guild={self.guild!r} '
|
||||||
'members={0.approximate_member_count}>'.format(self)
|
f'online={self.approximate_presence_count} '
|
||||||
|
f'members={self.approximate_member_count}>'
|
||||||
|
)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(self.code)
|
return hash(self.code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self) -> str:
|
||||||
""":class:`str`: Returns the proper code portion of the invite."""
|
""":class:`str`: Returns the proper code portion of the invite."""
|
||||||
return self.code
|
return self.code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
""":class:`str`: A property that retrieves the invite URL."""
|
""":class:`str`: A property that retrieves the invite URL."""
|
||||||
return self.BASE + '/' + self.code
|
return self.BASE + '/' + self.code
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
async def delete(self, *, reason: Optional[str] = None):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Revokes the instant invite.
|
Revokes the instant invite.
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
|
from typing import Awaitable, TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator
|
||||||
|
|
||||||
from .errors import NoMoreItems
|
from .errors import NoMoreItems
|
||||||
from .utils import time_snowflake, maybe_coroutine
|
from .utils import snowflake_time, time_snowflake, maybe_coroutine
|
||||||
from .object import Object
|
from .object import Object
|
||||||
from .audit_logs import AuditLogEntry
|
from .audit_logs import AuditLogEntry
|
||||||
|
|
||||||
@@ -42,24 +42,46 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .types.audit_log import (
|
||||||
|
AuditLog as AuditLogPayload,
|
||||||
|
)
|
||||||
|
from .types.guild import (
|
||||||
|
Guild as GuildPayload,
|
||||||
|
)
|
||||||
|
from .types.message import (
|
||||||
|
Message as MessagePayload,
|
||||||
|
)
|
||||||
|
from .types.user import (
|
||||||
|
PartialUser as PartialUserPayload,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .types.threads import (
|
||||||
|
Thread as ThreadPayload,
|
||||||
|
)
|
||||||
|
|
||||||
from .member import Member
|
from .member import Member
|
||||||
from .user import User
|
from .user import User
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .audit_logs import AuditLogEntry
|
from .audit_logs import AuditLogEntry
|
||||||
from .guild import Guild
|
from .guild import Guild
|
||||||
|
from .threads import Thread
|
||||||
|
from .abc import Snowflake
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
OT = TypeVar('OT')
|
OT = TypeVar('OT')
|
||||||
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
|
_Func = Callable[[T], Union[OT, Awaitable[OT]]]
|
||||||
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
|
|
||||||
|
|
||||||
OLDEST_OBJECT = Object(id=0)
|
OLDEST_OBJECT = Object(id=0)
|
||||||
|
|
||||||
|
|
||||||
class _AsyncIterator(AsyncIterator[T]):
|
class _AsyncIterator(AsyncIterator[T]):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def get(self, **attrs: Any) -> Optional[T]:
|
async def next(self) -> T:
|
||||||
def predicate(elem):
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get(self, **attrs: Any) -> Awaitable[Optional[T]]:
|
||||||
|
def predicate(elem: T):
|
||||||
for attr, val in attrs.items():
|
for attr, val in attrs.items():
|
||||||
nested = attr.split('__')
|
nested = attr.split('__')
|
||||||
obj = elem
|
obj = elem
|
||||||
@@ -72,7 +94,7 @@ class _AsyncIterator(AsyncIterator[T]):
|
|||||||
|
|
||||||
return self.find(predicate)
|
return self.find(predicate)
|
||||||
|
|
||||||
async def find(self, predicate: _Predicate[T]) -> Optional[T]:
|
async def find(self, predicate: _Func[T, bool]) -> Optional[T]:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
elem = await self.next()
|
elem = await self.next()
|
||||||
@@ -91,7 +113,7 @@ class _AsyncIterator(AsyncIterator[T]):
|
|||||||
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
|
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
|
||||||
return _MappedAsyncIterator(self, func)
|
return _MappedAsyncIterator(self, func)
|
||||||
|
|
||||||
def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
|
def filter(self, predicate: _Func[T, bool]) -> _FilteredAsyncIterator[T]:
|
||||||
return _FilteredAsyncIterator(self, predicate)
|
return _FilteredAsyncIterator(self, predicate)
|
||||||
|
|
||||||
async def flatten(self) -> List[T]:
|
async def flatten(self) -> List[T]:
|
||||||
@@ -103,16 +125,18 @@ class _AsyncIterator(AsyncIterator[T]):
|
|||||||
except NoMoreItems:
|
except NoMoreItems:
|
||||||
raise StopAsyncIteration()
|
raise StopAsyncIteration()
|
||||||
|
|
||||||
|
|
||||||
def _identity(x):
|
def _identity(x):
|
||||||
return x
|
return x
|
||||||
|
|
||||||
class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
|
||||||
|
class _ChunkedAsyncIterator(_AsyncIterator[List[T]]):
|
||||||
def __init__(self, iterator, max_size):
|
def __init__(self, iterator, max_size):
|
||||||
self.iterator = iterator
|
self.iterator = iterator
|
||||||
self.max_size = max_size
|
self.max_size = max_size
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> List[T]:
|
||||||
ret = []
|
ret: List[T] = []
|
||||||
n = 0
|
n = 0
|
||||||
while n < self.max_size:
|
while n < self.max_size:
|
||||||
try:
|
try:
|
||||||
@@ -126,6 +150,7 @@ class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
|||||||
n += 1
|
n += 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class _MappedAsyncIterator(_AsyncIterator[T]):
|
class _MappedAsyncIterator(_AsyncIterator[T]):
|
||||||
def __init__(self, iterator, func):
|
def __init__(self, iterator, func):
|
||||||
self.iterator = iterator
|
self.iterator = iterator
|
||||||
@@ -136,6 +161,7 @@ class _MappedAsyncIterator(_AsyncIterator[T]):
|
|||||||
item = await self.iterator.next()
|
item = await self.iterator.next()
|
||||||
return await maybe_coroutine(self.func, item)
|
return await maybe_coroutine(self.func, item)
|
||||||
|
|
||||||
|
|
||||||
class _FilteredAsyncIterator(_AsyncIterator[T]):
|
class _FilteredAsyncIterator(_AsyncIterator[T]):
|
||||||
def __init__(self, iterator, predicate):
|
def __init__(self, iterator, predicate):
|
||||||
self.iterator = iterator
|
self.iterator = iterator
|
||||||
@@ -155,6 +181,7 @@ class _FilteredAsyncIterator(_AsyncIterator[T]):
|
|||||||
if ret:
|
if ret:
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||||
def __init__(self, message, emoji, limit=100, after=None):
|
def __init__(self, message, emoji, limit=100, after=None):
|
||||||
self.message = message
|
self.message = message
|
||||||
@@ -168,7 +195,7 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
|||||||
self.channel_id = message.channel.id
|
self.channel_id = message.channel.id
|
||||||
self.users = asyncio.Queue()
|
self.users = asyncio.Queue()
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> Union[User, Member]:
|
||||||
if self.users.empty():
|
if self.users.empty():
|
||||||
await self.fill_users()
|
await self.fill_users()
|
||||||
|
|
||||||
@@ -185,7 +212,9 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
|||||||
retrieve = self.limit if self.limit <= 100 else 100
|
retrieve = self.limit if self.limit <= 100 else 100
|
||||||
|
|
||||||
after = self.after.id if self.after else None
|
after = self.after.id if self.after else None
|
||||||
data = await self.getter(self.channel_id, self.message.id, self.emoji, retrieve, after=after)
|
data: List[PartialUserPayload] = await self.getter(
|
||||||
|
self.channel_id, self.message.id, self.emoji, retrieve, after=after
|
||||||
|
)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
self.limit -= retrieve
|
self.limit -= retrieve
|
||||||
@@ -203,6 +232,7 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
|||||||
else:
|
else:
|
||||||
await self.users.put(User(state=self.state, data=element))
|
await self.users.put(User(state=self.state, data=element))
|
||||||
|
|
||||||
|
|
||||||
class HistoryIterator(_AsyncIterator['Message']):
|
class HistoryIterator(_AsyncIterator['Message']):
|
||||||
"""Iterator for receiving a channel's message history.
|
"""Iterator for receiving a channel's message history.
|
||||||
|
|
||||||
@@ -237,8 +267,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
``True`` if `after` is specified, otherwise ``False``.
|
``True`` if `after` is specified, otherwise ``False``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, messageable, limit,
|
def __init__(self, messageable, limit, before=None, after=None, around=None, oldest_first=None):
|
||||||
before=None, after=None, around=None, oldest_first=None):
|
|
||||||
|
|
||||||
if isinstance(before, datetime.datetime):
|
if isinstance(before, datetime.datetime):
|
||||||
before = Object(id=time_snowflake(before, high=False))
|
before = Object(id=time_snowflake(before, high=False))
|
||||||
@@ -272,7 +301,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
elif self.limit == 101:
|
elif self.limit == 101:
|
||||||
self.limit = 100 # Thanks discord
|
self.limit = 100 # Thanks discord
|
||||||
|
|
||||||
self._retrieve_messages = self._retrieve_messages_around_strategy
|
self._retrieve_messages = self._retrieve_messages_around_strategy # type: ignore
|
||||||
if self.before and self.after:
|
if self.before and self.after:
|
||||||
self._filter = lambda m: self.after.id < int(m['id']) < self.before.id
|
self._filter = lambda m: self.after.id < int(m['id']) < self.before.id
|
||||||
elif self.before:
|
elif self.before:
|
||||||
@@ -281,15 +310,15 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
self._filter = lambda m: self.after.id < int(m['id'])
|
self._filter = lambda m: self.after.id < int(m['id'])
|
||||||
else:
|
else:
|
||||||
if self.reverse:
|
if self.reverse:
|
||||||
self._retrieve_messages = self._retrieve_messages_after_strategy
|
self._retrieve_messages = self._retrieve_messages_after_strategy # type: ignore
|
||||||
if (self.before):
|
if self.before:
|
||||||
self._filter = lambda m: int(m['id']) < self.before.id
|
self._filter = lambda m: int(m['id']) < self.before.id
|
||||||
else:
|
else:
|
||||||
self._retrieve_messages = self._retrieve_messages_before_strategy
|
self._retrieve_messages = self._retrieve_messages_before_strategy # type: ignore
|
||||||
if (self.after and self.after != OLDEST_OBJECT):
|
if self.after and self.after != OLDEST_OBJECT:
|
||||||
self._filter = lambda m: int(m['id']) > self.after.id
|
self._filter = lambda m: int(m['id']) > self.after.id
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> Message:
|
||||||
if self.messages.empty():
|
if self.messages.empty():
|
||||||
await self.fill_messages()
|
await self.fill_messages()
|
||||||
|
|
||||||
@@ -316,7 +345,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
if self._get_retrieve():
|
if self._get_retrieve():
|
||||||
data = await self._retrieve_messages(self.retrieve)
|
data = await self._retrieve_messages(self.retrieve)
|
||||||
if len(data) < 100:
|
if len(data) < 100:
|
||||||
self.limit = 0 # terminate the infinite loop
|
self.limit = 0 # terminate the infinite loop
|
||||||
|
|
||||||
if self.reverse:
|
if self.reverse:
|
||||||
data = reversed(data)
|
data = reversed(data)
|
||||||
@@ -327,14 +356,14 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
for element in data:
|
for element in data:
|
||||||
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
||||||
|
|
||||||
async def _retrieve_messages(self, retrieve):
|
async def _retrieve_messages(self, retrieve) -> List[Message]:
|
||||||
"""Retrieve messages and update next parameters."""
|
"""Retrieve messages and update next parameters."""
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def _retrieve_messages_before_strategy(self, retrieve):
|
async def _retrieve_messages_before_strategy(self, retrieve):
|
||||||
"""Retrieve messages using before parameter."""
|
"""Retrieve messages using before parameter."""
|
||||||
before = self.before.id if self.before else None
|
before = self.before.id if self.before else None
|
||||||
data = await self.logs_from(self.channel.id, retrieve, before=before)
|
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, before=before)
|
||||||
if len(data):
|
if len(data):
|
||||||
if self.limit is not None:
|
if self.limit is not None:
|
||||||
self.limit -= retrieve
|
self.limit -= retrieve
|
||||||
@@ -344,7 +373,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
async def _retrieve_messages_after_strategy(self, retrieve):
|
async def _retrieve_messages_after_strategy(self, retrieve):
|
||||||
"""Retrieve messages using after parameter."""
|
"""Retrieve messages using after parameter."""
|
||||||
after = self.after.id if self.after else None
|
after = self.after.id if self.after else None
|
||||||
data = await self.logs_from(self.channel.id, retrieve, after=after)
|
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, after=after)
|
||||||
if len(data):
|
if len(data):
|
||||||
if self.limit is not None:
|
if self.limit is not None:
|
||||||
self.limit -= retrieve
|
self.limit -= retrieve
|
||||||
@@ -355,11 +384,12 @@ class HistoryIterator(_AsyncIterator['Message']):
|
|||||||
"""Retrieve messages using around parameter."""
|
"""Retrieve messages using around parameter."""
|
||||||
if self.around:
|
if self.around:
|
||||||
around = self.around.id if self.around else None
|
around = self.around.id if self.around else None
|
||||||
data = await self.logs_from(self.channel.id, retrieve, around=around)
|
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, around=around)
|
||||||
self.around = None
|
self.around = None
|
||||||
return data
|
return data
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||||
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
|
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
|
||||||
if isinstance(before, datetime.datetime):
|
if isinstance(before, datetime.datetime):
|
||||||
@@ -367,7 +397,6 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
if isinstance(after, datetime.datetime):
|
if isinstance(after, datetime.datetime):
|
||||||
after = Object(id=time_snowflake(after, high=True))
|
after = Object(id=time_snowflake(after, high=True))
|
||||||
|
|
||||||
|
|
||||||
if oldest_first is None:
|
if oldest_first is None:
|
||||||
self.reverse = after is not None
|
self.reverse = after is not None
|
||||||
else:
|
else:
|
||||||
@@ -384,12 +413,10 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
self._users = {}
|
self._users = {}
|
||||||
self._state = guild._state
|
self._state = guild._state
|
||||||
|
|
||||||
|
|
||||||
self._filter = None # entry dict -> bool
|
self._filter = None # entry dict -> bool
|
||||||
|
|
||||||
self.entries = asyncio.Queue()
|
self.entries = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
if self.reverse:
|
if self.reverse:
|
||||||
self._strategy = self._after_strategy
|
self._strategy = self._after_strategy
|
||||||
if self.before:
|
if self.before:
|
||||||
@@ -401,8 +428,9 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
|
|
||||||
async def _before_strategy(self, retrieve):
|
async def _before_strategy(self, retrieve):
|
||||||
before = self.before.id if self.before else None
|
before = self.before.id if self.before else None
|
||||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
data: AuditLogPayload = await self.request(
|
||||||
action_type=self.action_type, before=before)
|
self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, before=before
|
||||||
|
)
|
||||||
|
|
||||||
entries = data.get('audit_log_entries', [])
|
entries = data.get('audit_log_entries', [])
|
||||||
if len(data) and entries:
|
if len(data) and entries:
|
||||||
@@ -413,8 +441,9 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
|
|
||||||
async def _after_strategy(self, retrieve):
|
async def _after_strategy(self, retrieve):
|
||||||
after = self.after.id if self.after else None
|
after = self.after.id if self.after else None
|
||||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
data: AuditLogPayload = await self.request(
|
||||||
action_type=self.action_type, after=after)
|
self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, after=after
|
||||||
|
)
|
||||||
entries = data.get('audit_log_entries', [])
|
entries = data.get('audit_log_entries', [])
|
||||||
if len(data) and entries:
|
if len(data) and entries:
|
||||||
if self.limit is not None:
|
if self.limit is not None:
|
||||||
@@ -422,7 +451,7 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
self.after = Object(id=int(entries[0]['id']))
|
self.after = Object(id=int(entries[0]['id']))
|
||||||
return data.get('users', []), entries
|
return data.get('users', []), entries
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> AuditLogEntry:
|
||||||
if self.entries.empty():
|
if self.entries.empty():
|
||||||
await self._fill()
|
await self._fill()
|
||||||
|
|
||||||
@@ -446,7 +475,7 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
|||||||
if self._get_retrieve():
|
if self._get_retrieve():
|
||||||
users, data = await self._strategy(self.retrieve)
|
users, data = await self._strategy(self.retrieve)
|
||||||
if len(data) < 100:
|
if len(data) < 100:
|
||||||
self.limit = 0 # terminate the infinite loop
|
self.limit = 0 # terminate the infinite loop
|
||||||
|
|
||||||
if self.reverse:
|
if self.reverse:
|
||||||
data = reversed(data)
|
data = reversed(data)
|
||||||
@@ -493,6 +522,7 @@ class GuildIterator(_AsyncIterator['Guild']):
|
|||||||
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||||
Object after which all guilds must be.
|
Object after which all guilds must be.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot, limit, before=None, after=None):
|
def __init__(self, bot, limit, before=None, after=None):
|
||||||
|
|
||||||
if isinstance(before, datetime.datetime):
|
if isinstance(before, datetime.datetime):
|
||||||
@@ -512,14 +542,14 @@ class GuildIterator(_AsyncIterator['Guild']):
|
|||||||
self.guilds = asyncio.Queue()
|
self.guilds = asyncio.Queue()
|
||||||
|
|
||||||
if self.before and self.after:
|
if self.before and self.after:
|
||||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
|
||||||
self._filter = lambda m: int(m['id']) > self.after.id
|
self._filter = lambda m: int(m['id']) > self.after.id
|
||||||
elif self.after:
|
elif self.after:
|
||||||
self._retrieve_guilds = self._retrieve_guilds_after_strategy
|
self._retrieve_guilds = self._retrieve_guilds_after_strategy # type: ignore
|
||||||
else:
|
else:
|
||||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> Guild:
|
||||||
if self.guilds.empty():
|
if self.guilds.empty():
|
||||||
await self.fill_guilds()
|
await self.fill_guilds()
|
||||||
|
|
||||||
@@ -539,6 +569,7 @@ class GuildIterator(_AsyncIterator['Guild']):
|
|||||||
|
|
||||||
def create_guild(self, data):
|
def create_guild(self, data):
|
||||||
from .guild import Guild
|
from .guild import Guild
|
||||||
|
|
||||||
return Guild(state=self.state, data=data)
|
return Guild(state=self.state, data=data)
|
||||||
|
|
||||||
async def fill_guilds(self):
|
async def fill_guilds(self):
|
||||||
@@ -553,14 +584,14 @@ class GuildIterator(_AsyncIterator['Guild']):
|
|||||||
for element in data:
|
for element in data:
|
||||||
await self.guilds.put(self.create_guild(element))
|
await self.guilds.put(self.create_guild(element))
|
||||||
|
|
||||||
async def _retrieve_guilds(self, retrieve):
|
async def _retrieve_guilds(self, retrieve) -> List[Guild]:
|
||||||
"""Retrieve guilds and update next parameters."""
|
"""Retrieve guilds and update next parameters."""
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def _retrieve_guilds_before_strategy(self, retrieve):
|
async def _retrieve_guilds_before_strategy(self, retrieve):
|
||||||
"""Retrieve guilds using before parameter."""
|
"""Retrieve guilds using before parameter."""
|
||||||
before = self.before.id if self.before else None
|
before = self.before.id if self.before else None
|
||||||
data = await self.get_guilds(retrieve, before=before)
|
data: List[GuildPayload] = await self.get_guilds(retrieve, before=before)
|
||||||
if len(data):
|
if len(data):
|
||||||
if self.limit is not None:
|
if self.limit is not None:
|
||||||
self.limit -= retrieve
|
self.limit -= retrieve
|
||||||
@@ -570,13 +601,14 @@ class GuildIterator(_AsyncIterator['Guild']):
|
|||||||
async def _retrieve_guilds_after_strategy(self, retrieve):
|
async def _retrieve_guilds_after_strategy(self, retrieve):
|
||||||
"""Retrieve guilds using after parameter."""
|
"""Retrieve guilds using after parameter."""
|
||||||
after = self.after.id if self.after else None
|
after = self.after.id if self.after else None
|
||||||
data = await self.get_guilds(retrieve, after=after)
|
data: List[GuildPayload] = await self.get_guilds(retrieve, after=after)
|
||||||
if len(data):
|
if len(data):
|
||||||
if self.limit is not None:
|
if self.limit is not None:
|
||||||
self.limit -= retrieve
|
self.limit -= retrieve
|
||||||
self.after = Object(id=int(data[0]['id']))
|
self.after = Object(id=int(data[0]['id']))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class MemberIterator(_AsyncIterator['Member']):
|
class MemberIterator(_AsyncIterator['Member']):
|
||||||
def __init__(self, guild, limit=1000, after=None):
|
def __init__(self, guild, limit=1000, after=None):
|
||||||
|
|
||||||
@@ -591,7 +623,7 @@ class MemberIterator(_AsyncIterator['Member']):
|
|||||||
self.get_members = self.state.http.get_members
|
self.get_members = self.state.http.get_members
|
||||||
self.members = asyncio.Queue()
|
self.members = asyncio.Queue()
|
||||||
|
|
||||||
async def next(self) -> T:
|
async def next(self) -> Member:
|
||||||
if self.members.empty():
|
if self.members.empty():
|
||||||
await self.fill_members()
|
await self.fill_members()
|
||||||
|
|
||||||
@@ -618,7 +650,7 @@ class MemberIterator(_AsyncIterator['Member']):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if len(data) < 1000:
|
if len(data) < 1000:
|
||||||
self.limit = 0 # terminate loop
|
self.limit = 0 # terminate loop
|
||||||
|
|
||||||
self.after = Object(id=int(data[-1]['user']['id']))
|
self.after = Object(id=int(data[-1]['user']['id']))
|
||||||
|
|
||||||
@@ -627,4 +659,95 @@ class MemberIterator(_AsyncIterator['Member']):
|
|||||||
|
|
||||||
def create_member(self, data):
|
def create_member(self, data):
|
||||||
from .member import Member
|
from .member import Member
|
||||||
|
|
||||||
return Member(data=data, guild=self.guild, state=self.state)
|
return Member(data=data, guild=self.guild, state=self.state)
|
||||||
|
|
||||||
|
|
||||||
|
class ArchivedThreadIterator(_AsyncIterator['Thread']):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
channel_id: int,
|
||||||
|
guild: Guild,
|
||||||
|
limit: Optional[int],
|
||||||
|
joined: bool,
|
||||||
|
private: bool,
|
||||||
|
before: Optional[Union[Snowflake, datetime.datetime]] = None,
|
||||||
|
):
|
||||||
|
self.channel_id = channel_id
|
||||||
|
self.guild = guild
|
||||||
|
self.limit = limit
|
||||||
|
self.joined = joined
|
||||||
|
self.private = private
|
||||||
|
self.http = guild._state.http
|
||||||
|
|
||||||
|
if joined and not private:
|
||||||
|
raise ValueError('Cannot iterate over joined public archived threads')
|
||||||
|
|
||||||
|
self.before: Optional[str]
|
||||||
|
if before is None:
|
||||||
|
self.before = None
|
||||||
|
elif isinstance(before, datetime.datetime):
|
||||||
|
if joined:
|
||||||
|
self.before = str(time_snowflake(before, high=False))
|
||||||
|
else:
|
||||||
|
self.before = before.isoformat()
|
||||||
|
else:
|
||||||
|
if joined:
|
||||||
|
self.before = str(before.id)
|
||||||
|
else:
|
||||||
|
self.before = snowflake_time(before.id).isoformat()
|
||||||
|
|
||||||
|
self.update_before: Callable[[ThreadPayload], str] = self.get_archive_timestamp
|
||||||
|
|
||||||
|
if joined:
|
||||||
|
self.endpoint = self.http.get_joined_private_archived_threads
|
||||||
|
self.update_before = self.get_thread_id
|
||||||
|
elif private:
|
||||||
|
self.endpoint = self.http.get_private_archived_threads
|
||||||
|
else:
|
||||||
|
self.endpoint = self.http.get_public_archived_threads
|
||||||
|
|
||||||
|
self.queue: asyncio.Queue[Thread] = asyncio.Queue()
|
||||||
|
self.has_more: bool = True
|
||||||
|
|
||||||
|
async def next(self) -> Thread:
|
||||||
|
if self.queue.empty():
|
||||||
|
await self.fill_queue()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.queue.get_nowait()
|
||||||
|
except asyncio.QueueEmpty:
|
||||||
|
raise NoMoreItems()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_archive_timestamp(data: ThreadPayload) -> str:
|
||||||
|
return data['thread_metadata']['archive_timestamp']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_thread_id(data: ThreadPayload) -> str:
|
||||||
|
return data['id'] # type: ignore
|
||||||
|
|
||||||
|
async def fill_queue(self) -> None:
|
||||||
|
if not self.has_more:
|
||||||
|
raise NoMoreItems()
|
||||||
|
|
||||||
|
limit = 50 if self.limit is None else max(self.limit, 50)
|
||||||
|
data = await self.endpoint(self.channel_id, before=self.before, limit=limit)
|
||||||
|
|
||||||
|
# This stuff is obviously WIP because 'members' is always empty
|
||||||
|
threads: List[ThreadPayload] = data.get('threads', [])
|
||||||
|
for d in reversed(threads):
|
||||||
|
self.queue.put_nowait(self.create_thread(d))
|
||||||
|
|
||||||
|
self.has_more = data.get('has_more', False)
|
||||||
|
if self.limit is not None:
|
||||||
|
self.limit -= len(threads)
|
||||||
|
if self.limit <= 0:
|
||||||
|
self.has_more = False
|
||||||
|
|
||||||
|
if self.has_more:
|
||||||
|
self.before = self.update_before(threads[-1])
|
||||||
|
|
||||||
|
def create_thread(self, data: ThreadPayload) -> Thread:
|
||||||
|
from .threads import Thread
|
||||||
|
return Thread(guild=self.guild, data=data)
|
||||||
|
|||||||
@@ -22,16 +22,18 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
from typing import List, Literal, Optional, TYPE_CHECKING, Union, overload
|
||||||
|
|
||||||
import discord.abc
|
import discord.abc
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .errors import ClientException
|
|
||||||
from .user import BaseUser, User
|
from .user import BaseUser, User
|
||||||
from .activity import create_activity
|
from .activity import create_activity
|
||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
@@ -44,6 +46,12 @@ __all__ = (
|
|||||||
'Member',
|
'Member',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import VoiceChannel, StageChannel
|
||||||
|
from .abc import Snowflake
|
||||||
|
|
||||||
|
VocalGuildChannel = Union[VoiceChannel, StageChannel]
|
||||||
|
|
||||||
class VoiceState:
|
class VoiceState:
|
||||||
"""Represents a Discord user's voice state.
|
"""Represents a Discord user's voice state.
|
||||||
|
|
||||||
@@ -192,6 +200,13 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
|
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
|
||||||
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
|
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
|
||||||
The activities that the user is currently doing.
|
The activities that the user is currently doing.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Due to a Discord API limitation, a user's Spotify activity may not appear
|
||||||
|
if they are listening to a song with a title longer
|
||||||
|
than 128 characters. See :issue:`1738` for more information.
|
||||||
|
|
||||||
guild: :class:`Guild`
|
guild: :class:`Guild`
|
||||||
The guild that the member belongs to.
|
The guild that the member belongs to.
|
||||||
nick: Optional[:class:`str`]
|
nick: Optional[:class:`str`]
|
||||||
@@ -218,7 +233,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
self._client_status = {
|
self._client_status = {
|
||||||
None: 'offline'
|
None: 'offline'
|
||||||
}
|
}
|
||||||
self.activities = tuple(map(create_activity, data.get('activities', [])))
|
self.activities = []
|
||||||
self.nick = data.get('nick', None)
|
self.nick = data.get('nick', None)
|
||||||
self.pending = data.get('pending', False)
|
self.pending = data.get('pending', False)
|
||||||
|
|
||||||
@@ -262,17 +277,6 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
member_data['user'] = data
|
member_data['user'] = data
|
||||||
return cls(data=member_data, guild=guild, state=state)
|
return cls(data=member_data, guild=guild, state=state)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _from_presence_update(cls, *, data, guild, state):
|
|
||||||
clone = cls(data=data, guild=guild, state=state)
|
|
||||||
to_return = cls(data=data, guild=guild, state=state)
|
|
||||||
to_return._client_status = {
|
|
||||||
sys.intern(key): sys.intern(value)
|
|
||||||
for key, value in data.get('client_status', {}).items()
|
|
||||||
}
|
|
||||||
to_return._client_status[None] = sys.intern(data['status'])
|
|
||||||
return to_return, clone
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _copy(cls, member):
|
def _copy(cls, member):
|
||||||
self = cls.__new__(cls) # to bypass __init__
|
self = cls.__new__(cls) # to bypass __init__
|
||||||
@@ -316,7 +320,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
self._update_roles(data)
|
self._update_roles(data)
|
||||||
|
|
||||||
def _presence_update(self, data, user):
|
def _presence_update(self, data, user):
|
||||||
self.activities = tuple(map(create_activity, data.get('activities', [])))
|
self.activities = tuple(map(create_activity, data['activities']))
|
||||||
self._client_status = {
|
self._client_status = {
|
||||||
sys.intern(key): sys.intern(value)
|
sys.intern(key): sys.intern(value)
|
||||||
for key, value in data.get('client_status', {}).items()
|
for key, value in data.get('client_status', {}).items()
|
||||||
@@ -329,12 +333,12 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
|
|
||||||
def _update_inner_user(self, user):
|
def _update_inner_user(self, user):
|
||||||
u = self._user
|
u = self._user
|
||||||
original = (u.name, u.avatar, u.discriminator, u._public_flags)
|
original = (u.name, u._avatar, u.discriminator, u._public_flags)
|
||||||
# These keys seem to always be available
|
# These keys seem to always be available
|
||||||
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
|
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
|
||||||
if original != modified:
|
if original != modified:
|
||||||
to_return = User._copy(self._user)
|
to_return = User._copy(self._user)
|
||||||
u.name, u.avatar, u.discriminator, u._public_flags = modified
|
u.name, u._avatar, u.discriminator, u._public_flags = modified
|
||||||
# Signal to dispatch on_user_update
|
# Signal to dispatch on_user_update
|
||||||
return to_return, u
|
return to_return, u
|
||||||
|
|
||||||
@@ -444,6 +448,12 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
"""Union[:class:`BaseActivity`, :class:`Spotify`]: Returns the primary
|
"""Union[:class:`BaseActivity`, :class:`Spotify`]: Returns the primary
|
||||||
activity the user is currently doing. Could be ``None`` if no activity is being done.
|
activity the user is currently doing. Could be ``None`` if no activity is being done.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Due to a Discord API limitation, this may be ``None`` if
|
||||||
|
the user is listening to a song on Spotify with a title longer
|
||||||
|
than 128 characters. See :issue:`1738` for more information.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
A user may have multiple activities, these can be accessed under :attr:`activities`.
|
A user may have multiple activities, these can be accessed under :attr:`activities`.
|
||||||
@@ -472,27 +482,6 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
|
|
||||||
return any(self._roles.has(role.id) for role in message.role_mentions)
|
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`.
|
|
||||||
|
|
||||||
Basically equivalent to:
|
|
||||||
|
|
||||||
.. code-block:: python3
|
|
||||||
|
|
||||||
channel.permissions_for(self)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
channel: :class:`abc.GuildChannel`
|
|
||||||
The channel to check your permissions for.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`Permissions`
|
|
||||||
The resolved permissions for the member.
|
|
||||||
"""
|
|
||||||
return channel.permissions_for(self)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def top_role(self):
|
def top_role(self):
|
||||||
""":class:`Role`: Returns the member's highest role.
|
""":class:`Role`: Returns the member's highest role.
|
||||||
@@ -513,8 +502,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
This only takes into consideration the guild permissions
|
This only takes into consideration the guild permissions
|
||||||
and not most of the implied permissions or any of the
|
and not most of the implied permissions or any of the
|
||||||
channel permission overwrites. For 100% accurate permission
|
channel permission overwrites. For 100% accurate permission
|
||||||
calculation, please use either :meth:`permissions_in` or
|
calculation, please use :meth:`abc.GuildChannel.permissions_for`.
|
||||||
:meth:`abc.GuildChannel.permissions_for`.
|
|
||||||
|
|
||||||
This does take into consideration guild ownership and the
|
This does take into consideration guild ownership and the
|
||||||
administrator implication.
|
administrator implication.
|
||||||
@@ -537,6 +525,19 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
|
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
|
||||||
return self.guild._voice_state_for(self._user.id)
|
return self.guild._voice_state_for(self._user.id)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def ban(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
delete_message_days: Literal[1, 2, 3, 4, 5, 6, 7] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def ban(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def ban(self, **kwargs):
|
async def ban(self, **kwargs):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -544,20 +545,38 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
"""
|
"""
|
||||||
await self.guild.ban(self, **kwargs)
|
await self.guild.ban(self, **kwargs)
|
||||||
|
|
||||||
async def unban(self, *, reason=None):
|
async def unban(self, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Unbans this member. Equivalent to :meth:`Guild.unban`.
|
Unbans this member. Equivalent to :meth:`Guild.unban`.
|
||||||
"""
|
"""
|
||||||
await self.guild.unban(self, reason=reason)
|
await self.guild.unban(self, reason=reason)
|
||||||
|
|
||||||
async def kick(self, *, reason=None):
|
async def kick(self, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Kicks this member. Equivalent to :meth:`Guild.kick`.
|
Kicks this member. Equivalent to :meth:`Guild.kick`.
|
||||||
"""
|
"""
|
||||||
await self.guild.kick(self, reason=reason)
|
await self.guild.kick(self, reason=reason)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
nick: Optional[str] = None,
|
||||||
|
mute: bool = ...,
|
||||||
|
deafen: bool = ...,
|
||||||
|
suppress: bool = ...,
|
||||||
|
roles: Optional[List[discord.abc.Snowflake]] = ...,
|
||||||
|
voice_channel: Optional[VocalGuildChannel] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **fields):
|
async def edit(self, *, reason=None, **fields):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@@ -705,7 +724,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
else:
|
else:
|
||||||
await self._state.http.edit_my_voice_state(self.guild.id, payload)
|
await self._state.http.edit_my_voice_state(self.guild.id, payload)
|
||||||
|
|
||||||
async def move_to(self, channel, *, reason=None):
|
async def move_to(self, channel: VocalGuildChannel, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Moves a member to a new voice channel (they must be connected first).
|
Moves a member to a new voice channel (they must be connected first).
|
||||||
@@ -728,7 +747,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
"""
|
"""
|
||||||
await self.edit(voice_channel=channel, reason=reason)
|
await self.edit(voice_channel=channel, reason=reason)
|
||||||
|
|
||||||
async def add_roles(self, *roles, reason=None, atomic=True):
|
async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True):
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
Gives the member a number of :class:`Role`\s.
|
Gives the member a number of :class:`Role`\s.
|
||||||
@@ -767,7 +786,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
for role in roles:
|
for role in roles:
|
||||||
await req(guild_id, user_id, role.id, reason=reason)
|
await req(guild_id, user_id, role.id, reason=reason)
|
||||||
|
|
||||||
async def remove_roles(self, *roles, reason=None, atomic=True):
|
async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None:
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
Removes :class:`Role`\s from this member.
|
Removes :class:`Role`\s from this member.
|
||||||
@@ -811,3 +830,20 @@ class Member(discord.abc.Messageable, _BaseUser):
|
|||||||
user_id = self.id
|
user_id = self.id
|
||||||
for role in roles:
|
for role in roles:
|
||||||
await req(guild_id, user_id, role.id, reason=reason)
|
await req(guild_id, user_id, role.id, reason=reason)
|
||||||
|
|
||||||
|
def get_role(self, role_id: int) -> Optional[discord.Role]:
|
||||||
|
"""Returns a role with the given ID from roles which the member has.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
role_id: :class:`int`
|
||||||
|
The ID to search for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Optional[:class:`Role`]
|
||||||
|
The role or ``None`` if not found in the member's roles.
|
||||||
|
"""
|
||||||
|
return self.guild.get_role(role_id) if self._roles.has(role_id) else None
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Type, TypeVar, Union, List, TYPE_CHECKING, Any, Union
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AllowedMentions',
|
'AllowedMentions',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.message import AllowedMentions as AllowedMentionsPayload
|
||||||
|
from .abc import Snowflake
|
||||||
|
|
||||||
|
|
||||||
class _FakeBool:
|
class _FakeBool:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'True'
|
return 'True'
|
||||||
@@ -36,7 +44,11 @@ class _FakeBool:
|
|||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
default = _FakeBool()
|
|
||||||
|
default: Any = _FakeBool()
|
||||||
|
|
||||||
|
A = TypeVar('A', bound='AllowedMentions')
|
||||||
|
|
||||||
|
|
||||||
class AllowedMentions:
|
class AllowedMentions:
|
||||||
"""A class that represents what mentions are allowed in a message.
|
"""A class that represents what mentions are allowed in a message.
|
||||||
@@ -70,14 +82,21 @@ class AllowedMentions:
|
|||||||
|
|
||||||
__slots__ = ('everyone', 'users', 'roles', 'replied_user')
|
__slots__ = ('everyone', 'users', 'roles', 'replied_user')
|
||||||
|
|
||||||
def __init__(self, *, everyone=default, users=default, roles=default, replied_user=default):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
everyone: bool = default,
|
||||||
|
users: Union[bool, List[Snowflake]] = default,
|
||||||
|
roles: Union[bool, List[Snowflake]] = default,
|
||||||
|
replied_user: bool = default,
|
||||||
|
):
|
||||||
self.everyone = everyone
|
self.everyone = everyone
|
||||||
self.users = users
|
self.users = users
|
||||||
self.roles = roles
|
self.roles = roles
|
||||||
self.replied_user = replied_user
|
self.replied_user = replied_user
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls: Type[A]) -> A:
|
||||||
"""A factory method that returns a :class:`AllowedMentions` with all fields explicitly set to ``True``
|
"""A factory method that returns a :class:`AllowedMentions` with all fields explicitly set to ``True``
|
||||||
|
|
||||||
.. versionadded:: 1.5
|
.. versionadded:: 1.5
|
||||||
@@ -85,14 +104,14 @@ class AllowedMentions:
|
|||||||
return cls(everyone=True, users=True, roles=True, replied_user=True)
|
return cls(everyone=True, users=True, roles=True, replied_user=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def none(cls):
|
def none(cls: Type[A]) -> A:
|
||||||
"""A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``
|
"""A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``
|
||||||
|
|
||||||
.. versionadded:: 1.5
|
.. versionadded:: 1.5
|
||||||
"""
|
"""
|
||||||
return cls(everyone=False, users=False, roles=False, replied_user=False)
|
return cls(everyone=False, users=False, roles=False, replied_user=False)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self) -> AllowedMentionsPayload:
|
||||||
parse = []
|
parse = []
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
@@ -113,9 +132,9 @@ class AllowedMentions:
|
|||||||
data['replied_user'] = True
|
data['replied_user'] = True
|
||||||
|
|
||||||
data['parse'] = parse
|
data['parse'] = parse
|
||||||
return data
|
return data # type: ignore
|
||||||
|
|
||||||
def merge(self, other):
|
def merge(self, other: AllowedMentions) -> AllowedMentions:
|
||||||
# Creates a new AllowedMentions by merging from another one.
|
# Creates a new AllowedMentions by merging from another one.
|
||||||
# Merge is done by using the 'self' values unless explicitly
|
# Merge is done by using the 'self' values unless explicitly
|
||||||
# overridden by the 'other' values.
|
# overridden by the 'other' values.
|
||||||
@@ -125,5 +144,8 @@ class AllowedMentions:
|
|||||||
replied_user = self.replied_user if other.replied_user is default else other.replied_user
|
replied_user = self.replied_user if other.replied_user is default else other.replied_user
|
||||||
return AllowedMentions(everyone=everyone, roles=roles, users=users, replied_user=replied_user)
|
return AllowedMentions(everyone=everyone, roles=roles, users=users, replied_user=replied_user)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '{0.__class__.__qualname__}(everyone={0.everyone}, users={0.users}, roles={0.roles}, replied_user={0.replied_user})'.format(self)
|
return (
|
||||||
|
f'{self.__class__.__name__}(everyone={self.everyone}, '
|
||||||
|
f'users={self.users}, roles={self.roles}, replied_user={self.replied_user})'
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,18 +22,24 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'EqualityComparable',
|
'EqualityComparable',
|
||||||
'Hashable',
|
'Hashable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
E = TypeVar('E', bound='EqualityComparable')
|
||||||
|
|
||||||
class EqualityComparable:
|
class EqualityComparable:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __eq__(self, other):
|
id: int
|
||||||
|
|
||||||
|
def __eq__(self: E, other: E) -> bool:
|
||||||
return isinstance(other, self.__class__) and other.id == self.id
|
return isinstance(other, self.__class__) and other.id == self.id
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self: E, other: E) -> bool:
|
||||||
if isinstance(other, self.__class__):
|
if isinstance(other, self.__class__):
|
||||||
return other.id != self.id
|
return other.id != self.id
|
||||||
return True
|
return True
|
||||||
@@ -41,5 +47,5 @@ class EqualityComparable:
|
|||||||
class Hashable(EqualityComparable):
|
class Hashable(EqualityComparable):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return self.id >> 22
|
return self.id >> 22
|
||||||
|
|||||||
@@ -22,9 +22,21 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
SupportsInt,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime
|
||||||
|
SupportsIntCast = Union[SupportsInt, str, bytes, bytearray]
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Object',
|
'Object',
|
||||||
)
|
)
|
||||||
@@ -63,7 +75,7 @@ class Object(Hashable):
|
|||||||
The ID of the object.
|
The ID of the object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id):
|
def __init__(self, id: SupportsIntCast):
|
||||||
try:
|
try:
|
||||||
id = int(id)
|
id = int(id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -71,10 +83,10 @@ class Object(Hashable):
|
|||||||
else:
|
else:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f'<Object id={self.id!r}>'
|
return f'<Object id={self.id!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the snowflake's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the snowflake's creation time in UTC."""
|
||||||
return utils.snowflake_time(self.id)
|
return utils.snowflake_time(self.id)
|
||||||
|
|||||||
@@ -22,17 +22,37 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .asset import Asset
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .asset import Asset, AssetMixin
|
||||||
|
from .errors import InvalidArgument
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PartialEmoji',
|
'PartialEmoji',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .state import ConnectionState
|
||||||
|
from datetime import datetime
|
||||||
|
from .types.message import PartialEmoji as PartialEmojiPayload
|
||||||
|
|
||||||
class _EmojiTag:
|
class _EmojiTag:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
class PartialEmoji(_EmojiTag):
|
id: int
|
||||||
|
|
||||||
|
def _to_partial(self) -> PartialEmoji:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
PE = TypeVar('PE', bound='PartialEmoji')
|
||||||
|
|
||||||
|
|
||||||
|
class PartialEmoji(_EmojiTag, AssetMixin):
|
||||||
"""Represents a "partial" emoji.
|
"""Represents a "partial" emoji.
|
||||||
|
|
||||||
This model will be given in two scenarios:
|
This model will be given in two scenarios:
|
||||||
@@ -72,35 +92,80 @@ class PartialEmoji(_EmojiTag):
|
|||||||
|
|
||||||
__slots__ = ('animated', 'name', 'id', '_state')
|
__slots__ = ('animated', 'name', 'id', '_state')
|
||||||
|
|
||||||
def __init__(self, *, name, animated=False, id=None):
|
_CUSTOM_EMOJI_RE = re.compile(r'<?(?P<animated>a)?:?(?P<name>[A-Za-z0-9\_]+):(?P<id>[0-9]{13,20})>?')
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
id: Optional[int]
|
||||||
|
|
||||||
|
def __init__(self, *, name: str, animated: bool = False, id: Optional[int] = None):
|
||||||
self.animated = animated
|
self.animated = animated
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self._state = None
|
self._state: Optional[ConnectionState] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data):
|
def from_dict(cls: Type[PE], data: Dict[str, Any]) -> PE:
|
||||||
return cls(
|
return cls(
|
||||||
animated=data.get('animated', False),
|
animated=data.get('animated', False),
|
||||||
id=utils._get_as_snowflake(data, 'id'),
|
id=utils._get_as_snowflake(data, 'id'),
|
||||||
name=data.get('name'),
|
name=data.get('name', ''),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
@classmethod
|
||||||
o = { 'name': self.name }
|
def from_str(cls: Type[PE], value: str) -> PE:
|
||||||
|
"""Converts a Discord string representation of an emoji to a :class:`PartialEmoji`.
|
||||||
|
|
||||||
|
The formats accepted are:
|
||||||
|
|
||||||
|
- ``a:name:id``
|
||||||
|
- ``<a:name:id>``
|
||||||
|
- ``name:id``
|
||||||
|
- ``<:name:id>``
|
||||||
|
|
||||||
|
If the format does not match then it is assumed to be a unicode emoji.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
value: :class:`str`
|
||||||
|
The string representation of an emoji.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`PartialEmoji`
|
||||||
|
The partial emoji from this string.
|
||||||
|
"""
|
||||||
|
match = cls._CUSTOM_EMOJI_RE.match(value)
|
||||||
|
if match is not None:
|
||||||
|
groups = match.groupdict()
|
||||||
|
animated = bool(groups['animated'])
|
||||||
|
emoji_id = int(groups['id'])
|
||||||
|
name = groups['name']
|
||||||
|
return cls(name=name, animated=animated, id=emoji_id)
|
||||||
|
|
||||||
|
return cls(name=value, id=None, animated=False)
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
o: Dict[str, Any] = {'name': self.name}
|
||||||
if self.id:
|
if self.id:
|
||||||
o['id'] = self.id
|
o['id'] = self.id
|
||||||
if self.animated:
|
if self.animated:
|
||||||
o['animated'] = self.animated
|
o['animated'] = self.animated
|
||||||
return o
|
return o
|
||||||
|
|
||||||
|
def _to_partial(self) -> PartialEmoji:
|
||||||
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def with_state(cls, state, *, name, animated=False, id=None):
|
def with_state(
|
||||||
|
cls: Type[PE], state: ConnectionState, *, name: str, animated: bool = False, id: Optional[int] = None
|
||||||
|
) -> PE:
|
||||||
self = cls(name=name, animated=animated, id=id)
|
self = cls(name=name, animated=animated, id=id)
|
||||||
self._state = state
|
self._state = state
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
return self.name
|
return self.name
|
||||||
if self.animated:
|
if self.animated:
|
||||||
@@ -108,9 +173,9 @@ class PartialEmoji(_EmojiTag):
|
|||||||
return f'<:{self.name}:{self.id}>'
|
return f'<:{self.name}:{self.id}>'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
|
return f'<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: Any) -> bool:
|
||||||
if self.is_unicode_emoji():
|
if self.is_unicode_emoji():
|
||||||
return isinstance(other, PartialEmoji) and self.name == other.name
|
return isinstance(other, PartialEmoji) and self.name == other.name
|
||||||
|
|
||||||
@@ -118,75 +183,50 @@ class PartialEmoji(_EmojiTag):
|
|||||||
return self.id == other.id
|
return self.id == other.id
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other: Any) -> bool:
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash((self.id, self.name))
|
return hash((self.id, self.name))
|
||||||
|
|
||||||
def is_custom_emoji(self):
|
def is_custom_emoji(self) -> bool:
|
||||||
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
|
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
|
||||||
return self.id is not None
|
return self.id is not None
|
||||||
|
|
||||||
def is_unicode_emoji(self):
|
def is_unicode_emoji(self) -> bool:
|
||||||
""":class:`bool`: Checks if this is a Unicode emoji."""
|
""":class:`bool`: Checks if this is a Unicode emoji."""
|
||||||
return self.id is None
|
return self.id is None
|
||||||
|
|
||||||
def _as_reaction(self):
|
def _as_reaction(self) -> str:
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
return self.name
|
return self.name
|
||||||
return f'{self.name}:{self.id}'
|
return f'{self.name}:{self.id}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> Optional[datetime]:
|
||||||
"""Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji.
|
"""Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
"""
|
"""
|
||||||
if self.is_unicode_emoji():
|
if self.id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return utils.snowflake_time(self.id)
|
return utils.snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
""":class:`Asset`: Returns the asset of the emoji, if it is custom.
|
""":class:`str`: Returns the URL of the emoji, if it is custom.
|
||||||
|
|
||||||
This is equivalent to calling :meth:`url_as` with
|
If this isn't a custom emoji then an empty string is returned
|
||||||
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():
|
if self.is_unicode_emoji():
|
||||||
return Asset(self._state)
|
return ''
|
||||||
|
|
||||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
fmt = 'gif' if self.animated else 'png'
|
||||||
|
return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
|
||||||
|
|
||||||
|
async def read(self) -> bytes:
|
||||||
|
if self.is_unicode_emoji():
|
||||||
|
raise InvalidArgument('PartialEmoji is not a custom emoji')
|
||||||
|
|
||||||
|
return await super().read()
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class Permissions(BaseFlags):
|
|||||||
"""A factory method that creates a :class:`Permissions` with all
|
"""A factory method that creates a :class:`Permissions` with all
|
||||||
permissions set to ``True``.
|
permissions set to ``True``.
|
||||||
"""
|
"""
|
||||||
return cls(0b111111111111111111111111111111111)
|
return cls(0b111111111111111111111111111111111111)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_channel(cls):
|
def all_channel(cls):
|
||||||
@@ -471,6 +471,39 @@ class Permissions(BaseFlags):
|
|||||||
"""
|
"""
|
||||||
return 1 << 32
|
return 1 << 32
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def manage_events(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if a user can manage guild events.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 1 << 33
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def manage_threads(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if a user can manage threads.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 1 << 34
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def use_threads(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if a user can create and participate in public threads.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 1 << 35
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def use_private_threads(self):
|
||||||
|
""":class:`bool`: Returns ``True`` if a user can create and participate in private threads.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return 1 << 36
|
||||||
|
|
||||||
|
|
||||||
def augment_from_permissions(cls):
|
def augment_from_permissions(cls):
|
||||||
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
|
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
|
||||||
aliases = set()
|
aliases = set()
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class AudioSource:
|
|||||||
If the audio is complete, then returning an empty
|
If the audio is complete, then returning an empty
|
||||||
:term:`py:bytes-like object` to signal this is the way to do so.
|
:term:`py:bytes-like object` to signal this is the way to do so.
|
||||||
|
|
||||||
If :meth:`is_opus` method returns ``True``, then it must return
|
If :meth:`~AudioSource.is_opus` method returns ``True``, then it must return
|
||||||
20ms worth of Opus encoded audio. Otherwise, it must be 20ms
|
20ms worth of Opus encoded audio. Otherwise, it must be 20ms
|
||||||
worth of 16-bit 48KHz stereo PCM, which is about 3,840 bytes
|
worth of 16-bit 48KHz stereo PCM, which is about 3,840 bytes
|
||||||
per frame (20ms worth of audio).
|
per frame (20ms worth of audio).
|
||||||
@@ -144,7 +144,7 @@ class FFmpegAudio(AudioSource):
|
|||||||
executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
|
executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
|
||||||
raise ClientException(executable + ' was not found.') from None
|
raise ClientException(executable + ' was not found.') from None
|
||||||
except subprocess.SubprocessError as exc:
|
except subprocess.SubprocessError as exc:
|
||||||
raise ClientException('Popen failed: {0.__class__.__name__}: {0}'.format(exc)) from exc
|
raise ClientException(f'Popen failed: {exc.__class__.__name__}: {exc}') from exc
|
||||||
else:
|
else:
|
||||||
return process
|
return process
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ __all__ = (
|
|||||||
'RawReactionActionEvent',
|
'RawReactionActionEvent',
|
||||||
'RawReactionClearEvent',
|
'RawReactionClearEvent',
|
||||||
'RawReactionClearEmojiEvent',
|
'RawReactionClearEmojiEvent',
|
||||||
|
'RawIntegrationDeleteEvent',
|
||||||
)
|
)
|
||||||
|
|
||||||
class _RawReprMixin:
|
class _RawReprMixin:
|
||||||
@@ -222,3 +223,29 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
|
|||||||
self.guild_id = int(data['guild_id'])
|
self.guild_id = int(data['guild_id'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.guild_id = None
|
self.guild_id = None
|
||||||
|
|
||||||
|
class RawIntegrationDeleteEvent(_RawReprMixin):
|
||||||
|
"""Represents the payload for a :func:`on_raw_integration_delete` event.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
integration_id: :class:`int`
|
||||||
|
The ID of the integration that got deleted.
|
||||||
|
application_id: Optional[:class:`int`]
|
||||||
|
The ID of the bot/OAuth2 application for this deleted integration.
|
||||||
|
guild_id: :class:`int`
|
||||||
|
The guild ID where the integration got deleted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('integration_id', 'application_id', 'guild_id')
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self.integration_id = int(data['id'])
|
||||||
|
self.guild_id = int(data['guild_id'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.application_id = int(data['application_id'])
|
||||||
|
except KeyError:
|
||||||
|
self.application_id = None
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Reaction:
|
|||||||
return str(self.emoji)
|
return str(self.emoji)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>'.format(self)
|
return f'<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>'
|
||||||
|
|
||||||
async def remove(self, user):
|
async def remove(self, user):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@@ -158,14 +158,14 @@ class Reaction:
|
|||||||
|
|
||||||
# I do not actually recommend doing this.
|
# I do not actually recommend doing this.
|
||||||
async for user in reaction.users():
|
async for user in reaction.users():
|
||||||
await channel.send('{0} has reacted with {1.emoji}!'.format(user, reaction))
|
await channel.send(f'{user} has reacted with {reaction.emoji}!')
|
||||||
|
|
||||||
Flattening into a list: ::
|
Flattening into a list: ::
|
||||||
|
|
||||||
users = await reaction.users().flatten()
|
users = await reaction.users().flatten()
|
||||||
# users is now a list of User...
|
# users is now a list of User...
|
||||||
winner = random.choice(users)
|
winner = random.choice(users)
|
||||||
await channel.send('{} has won the raffle.'.format(winner))
|
await channel.send(f'{winner} has won the raffle.')
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
------------
|
------------
|
||||||
@@ -191,7 +191,7 @@ class Reaction:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self.custom_emoji:
|
if self.custom_emoji:
|
||||||
emoji = '{0.name}:{0.id}'.format(self.emoji)
|
emoji = f'{self.emoji.name}:{self.emoji.id}'
|
||||||
else:
|
else:
|
||||||
emoji = self.emoji
|
emoji = self.emoji
|
||||||
|
|
||||||
|
|||||||
167
discord/role.py
167
discord/role.py
@@ -22,17 +22,31 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, List, Optional, TypeVar, Union, overload, TYPE_CHECKING
|
||||||
|
|
||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
from .errors import InvalidArgument
|
from .errors import InvalidArgument
|
||||||
from .colour import Colour
|
from .colour import Colour
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
from .utils import snowflake_time, _get_as_snowflake
|
from .utils import snowflake_time, _get_as_snowflake, MISSING
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RoleTags',
|
'RoleTags',
|
||||||
'Role',
|
'Role',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime
|
||||||
|
from .types.role import (
|
||||||
|
Role as RolePayload,
|
||||||
|
RoleTags as RoleTagPayload,
|
||||||
|
)
|
||||||
|
from .guild import Guild
|
||||||
|
from .member import Member
|
||||||
|
from .state import ConnectionState
|
||||||
|
|
||||||
|
|
||||||
class RoleTags:
|
class RoleTags:
|
||||||
"""Represents tags on a role.
|
"""Represents tags on a role.
|
||||||
|
|
||||||
@@ -52,32 +66,42 @@ class RoleTags:
|
|||||||
The integration ID that manages the role.
|
The integration ID that manages the role.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('bot_id', 'integration_id', '_premium_subscriber',)
|
__slots__ = (
|
||||||
|
'bot_id',
|
||||||
|
'integration_id',
|
||||||
|
'_premium_subscriber',
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data: RoleTagPayload):
|
||||||
self.bot_id = _get_as_snowflake(data, 'bot_id')
|
self.bot_id: Optional[int] = _get_as_snowflake(data, 'bot_id')
|
||||||
self.integration_id = _get_as_snowflake(data, 'integration_id')
|
self.integration_id: Optional[int] = _get_as_snowflake(data, 'integration_id')
|
||||||
# NOTE: The API returns "null" for this if it's valid, which corresponds to None.
|
# NOTE: The API returns "null" for this if it's valid, which corresponds to None.
|
||||||
# This is different from other fields where "null" means "not there".
|
# This is different from other fields where "null" means "not there".
|
||||||
# So in this case, a value of None is the same as True.
|
# So in this case, a value of None is the same as True.
|
||||||
# Which means we would need a different sentinel. For this purpose I used ellipsis.
|
# Which means we would need a different sentinel.
|
||||||
self._premium_subscriber = data.get('premium_subscriber', ...)
|
self._premium_subscriber: Optional[Any] = data.get('premium_subscriber', MISSING)
|
||||||
|
|
||||||
def is_bot_managed(self):
|
def is_bot_managed(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is associated with a bot."""
|
""":class:`bool`: Whether the role is associated with a bot."""
|
||||||
return self.bot_id is not None
|
return self.bot_id is not None
|
||||||
|
|
||||||
def is_premium_subscriber(self):
|
def is_premium_subscriber(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild."""
|
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild."""
|
||||||
return self._premium_subscriber is None
|
return self._premium_subscriber is None
|
||||||
|
|
||||||
def is_integration(self):
|
def is_integration(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is managed by an integration."""
|
""":class:`bool`: Whether the role is managed by an integration."""
|
||||||
return self.integration_id is not None
|
return self.integration_id is not None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<RoleTags bot_id={0.bot_id} integration_id={0.integration_id} ' \
|
return (
|
||||||
'premium_subscriber={1}>'.format(self, self.is_premium_subscriber())
|
f'<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} '
|
||||||
|
f'premium_subscriber={self.is_premium_subscriber()}>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
R = TypeVar('R', bound='Role')
|
||||||
|
|
||||||
|
|
||||||
class Role(Hashable):
|
class Role(Hashable):
|
||||||
"""Represents a Discord role in a :class:`Guild`.
|
"""Represents a Discord role in a :class:`Guild`.
|
||||||
@@ -129,6 +153,15 @@ class Role(Hashable):
|
|||||||
position: :class:`int`
|
position: :class:`int`
|
||||||
The position of the role. This number is usually positive. The bottom
|
The position of the role. This number is usually positive. The bottom
|
||||||
role has a position of 0.
|
role has a position of 0.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Multiple roles can have the same position number. As a consequence
|
||||||
|
of this, comparing via role position is prone to subtle bugs if
|
||||||
|
checking for role hierarchy. The recommended and correct way to
|
||||||
|
compare for roles in the hierarchy is using the comparison
|
||||||
|
operators on the role objects themselves.
|
||||||
|
|
||||||
managed: :class:`bool`
|
managed: :class:`bool`
|
||||||
Indicates if the role is managed by the guild through some form of
|
Indicates if the role is managed by the guild through some form of
|
||||||
integrations such as Twitch.
|
integrations such as Twitch.
|
||||||
@@ -138,22 +171,33 @@ class Role(Hashable):
|
|||||||
The role tags associated with this role.
|
The role tags associated with this role.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('id', 'name', '_permissions', '_colour', 'position',
|
__slots__ = (
|
||||||
'managed', 'mentionable', 'hoist', 'guild', 'tags', '_state')
|
'id',
|
||||||
|
'name',
|
||||||
|
'_permissions',
|
||||||
|
'_colour',
|
||||||
|
'position',
|
||||||
|
'managed',
|
||||||
|
'mentionable',
|
||||||
|
'hoist',
|
||||||
|
'guild',
|
||||||
|
'tags',
|
||||||
|
'_state',
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *, guild, state, data):
|
def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload):
|
||||||
self.guild = guild
|
self.guild: Guild = guild
|
||||||
self._state = state
|
self._state: ConnectionState = state
|
||||||
self.id = int(data['id'])
|
self.id: int = int(data['id'])
|
||||||
self._update(data)
|
self._update(data)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Role id={0.id} name={0.name!r}>'.format(self)
|
return f'<Role id={self.id} name={self.name!r}>'
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self: R, other: R) -> bool:
|
||||||
if not isinstance(other, Role) or not isinstance(self, Role):
|
if not isinstance(other, Role) or not isinstance(self, Role):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
@@ -174,87 +218,96 @@ class Role(Hashable):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self: R, other: R) -> bool:
|
||||||
r = Role.__lt__(other, self)
|
r = Role.__lt__(other, self)
|
||||||
if r is NotImplemented:
|
if r is NotImplemented:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return not r
|
return not r
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self: R, other: R) -> bool:
|
||||||
return Role.__lt__(other, self)
|
return Role.__lt__(other, self)
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self: R, other: R) -> bool:
|
||||||
r = Role.__lt__(self, other)
|
r = Role.__lt__(self, other)
|
||||||
if r is NotImplemented:
|
if r is NotImplemented:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return not r
|
return not r
|
||||||
|
|
||||||
def _update(self, data):
|
def _update(self, data: RolePayload):
|
||||||
self.name = data['name']
|
self.name: str = data['name']
|
||||||
self._permissions = int(data.get('permissions_new', 0))
|
self._permissions: int = int(data.get('permissions', 0))
|
||||||
self.position = data.get('position', 0)
|
self.position: int = data.get('position', 0)
|
||||||
self._colour = data.get('color', 0)
|
self._colour: int = data.get('color', 0)
|
||||||
self.hoist = data.get('hoist', False)
|
self.hoist: bool = data.get('hoist', False)
|
||||||
self.managed = data.get('managed', False)
|
self.managed: bool = data.get('managed', False)
|
||||||
self.mentionable = data.get('mentionable', False)
|
self.mentionable: bool = data.get('mentionable', False)
|
||||||
|
self.tags: Optional[RoleTags]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.tags = RoleTags(data['tags'])
|
self.tags = RoleTags(data['tags'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.tags = None
|
self.tags = None
|
||||||
|
|
||||||
def is_default(self):
|
def is_default(self) -> bool:
|
||||||
""":class:`bool`: Checks if the role is the default role."""
|
""":class:`bool`: Checks if the role is the default role."""
|
||||||
return self.guild.id == self.id
|
return self.guild.id == self.id
|
||||||
|
|
||||||
def is_bot_managed(self):
|
def is_bot_managed(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is associated with a bot.
|
""":class:`bool`: Whether the role is associated with a bot.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
"""
|
"""
|
||||||
return self.tags is not None and self.tags.is_bot_managed()
|
return self.tags is not None and self.tags.is_bot_managed()
|
||||||
|
|
||||||
def is_premium_subscriber(self):
|
def is_premium_subscriber(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.
|
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
"""
|
"""
|
||||||
return self.tags is not None and self.tags.is_premium_subscriber()
|
return self.tags is not None and self.tags.is_premium_subscriber()
|
||||||
|
|
||||||
def is_integration(self):
|
def is_integration(self) -> bool:
|
||||||
""":class:`bool`: Whether the role is managed by an integration.
|
""":class:`bool`: Whether the role is managed by an integration.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
"""
|
"""
|
||||||
return self.tags is not None and self.tags.is_integration()
|
return self.tags is not None and self.tags.is_integration()
|
||||||
|
|
||||||
|
def is_assignable(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the role is able to be assigned or removed by the bot.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
me = self.guild.me
|
||||||
|
return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def permissions(self):
|
def permissions(self) -> Permissions:
|
||||||
""":class:`Permissions`: Returns the role's permissions."""
|
""":class:`Permissions`: Returns the role's permissions."""
|
||||||
return Permissions(self._permissions)
|
return Permissions(self._permissions)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colour(self):
|
def colour(self) -> Colour:
|
||||||
""":class:`Colour`: Returns the role colour. An alias exists under ``color``."""
|
""":class:`Colour`: Returns the role colour. An alias exists under ``color``."""
|
||||||
return Colour(self._colour)
|
return Colour(self._colour)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self) -> Colour:
|
||||||
""":class:`Colour`: Returns the role color. An alias exists under ``colour``."""
|
""":class:`Colour`: Returns the role color. An alias exists under ``colour``."""
|
||||||
return self.colour
|
return self.colour
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the role's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the role's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mention(self):
|
def mention(self) -> str:
|
||||||
""":class:`str`: Returns a string that allows you to mention a role."""
|
""":class:`str`: Returns a string that allows you to mention a role."""
|
||||||
return f'<@&{self.id}>'
|
return f'<@&{self.id}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def members(self):
|
def members(self) -> List[Member]:
|
||||||
"""List[:class:`Member`]: Returns all the members with this role."""
|
"""List[:class:`Member`]: Returns all the members with this role."""
|
||||||
all_members = self.guild.members
|
all_members = self.guild.members
|
||||||
if self.is_default():
|
if self.is_default():
|
||||||
@@ -263,7 +316,7 @@ class Role(Hashable):
|
|||||||
role_id = self.id
|
role_id = self.id
|
||||||
return [member for member in all_members if member._roles.has(role_id)]
|
return [member for member in all_members if member._roles.has(role_id)]
|
||||||
|
|
||||||
async def _move(self, position, reason):
|
async def _move(self, position: int, reason: Optional[str]) -> None:
|
||||||
if position <= 0:
|
if position <= 0:
|
||||||
raise InvalidArgument("Cannot move role to position 0 or below")
|
raise InvalidArgument("Cannot move role to position 0 or below")
|
||||||
|
|
||||||
@@ -286,7 +339,25 @@ class Role(Hashable):
|
|||||||
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
|
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
|
||||||
await http.move_role_position(self.guild.id, payload, reason=reason)
|
await http.move_role_position(self.guild.id, payload, reason=reason)
|
||||||
|
|
||||||
async def edit(self, *, reason=None, **fields):
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = ...,
|
||||||
|
name: str = ...,
|
||||||
|
permissions: Permissions = ...,
|
||||||
|
colour: Union[Colour, int] = ...,
|
||||||
|
hoist: bool = ...,
|
||||||
|
mentionable: bool = ...,
|
||||||
|
position: int = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def edit(self, *, reason=None, **fields) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Edits the role.
|
Edits the role.
|
||||||
@@ -346,13 +417,13 @@ class Role(Hashable):
|
|||||||
'permissions': str(fields.get('permissions', self.permissions).value),
|
'permissions': str(fields.get('permissions', self.permissions).value),
|
||||||
'color': colour.value,
|
'color': colour.value,
|
||||||
'hoist': fields.get('hoist', self.hoist),
|
'hoist': fields.get('hoist', self.hoist),
|
||||||
'mentionable': fields.get('mentionable', self.mentionable)
|
'mentionable': fields.get('mentionable', self.mentionable),
|
||||||
}
|
}
|
||||||
|
|
||||||
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
|
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
|
||||||
self._update(data)
|
self._update(data)
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Deletes the role.
|
Deletes the role.
|
||||||
|
|||||||
@@ -359,42 +359,6 @@ class AutoShardedClient(Client):
|
|||||||
"""Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object."""
|
"""Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object."""
|
||||||
return { shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items() }
|
return { shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items() }
|
||||||
|
|
||||||
@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
|
|
||||||
-------
|
|
||||||
InvalidArgument
|
|
||||||
If any guild is unavailable in the collection.
|
|
||||||
"""
|
|
||||||
if any(g.unavailable for g in guilds):
|
|
||||||
raise InvalidArgument('An unavailable or non-large guild was passed.')
|
|
||||||
|
|
||||||
_guilds = sorted(guilds, key=lambda g: g.shard_id)
|
|
||||||
for shard_id, sub_guilds in itertools.groupby(_guilds, key=lambda g: g.shard_id):
|
|
||||||
for guild in sub_guilds:
|
|
||||||
await self._connection.chunk_guild(guild)
|
|
||||||
|
|
||||||
async def launch_shard(self, gateway, shard_id, *, initial=False):
|
async def launch_shard(self, gateway, shard_id, *, initial=False):
|
||||||
try:
|
try:
|
||||||
coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id)
|
coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id)
|
||||||
|
|||||||
168
discord/stage_instance.py
Normal file
168
discord/stage_instance.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from .utils import MISSING, cached_slot_property
|
||||||
|
from .mixins import Hashable
|
||||||
|
from .errors import InvalidArgument
|
||||||
|
from .enums import StagePrivacyLevel, try_enum
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'StageInstance',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.channel import StageInstance as StageInstancePayload
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .channel import StageChannel
|
||||||
|
from .guild import Guild
|
||||||
|
|
||||||
|
|
||||||
|
class StageInstance(Hashable):
|
||||||
|
"""Represents a stage instance of a stage channel in a guild.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two stagea instances are equal.
|
||||||
|
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two stage instances are not equal.
|
||||||
|
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Returns the stage instance's hash.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`int`
|
||||||
|
The stage instance's ID.
|
||||||
|
guild: :class:`Guild`
|
||||||
|
The guild that the stage instance is running in.
|
||||||
|
channel_id: :class:`int`
|
||||||
|
The ID of the channel that the stage instance is running in.
|
||||||
|
topic: :class:`str`
|
||||||
|
The topic of the stage instance.
|
||||||
|
privacy_level: :class:`StagePrivacyLevel`
|
||||||
|
The privacy level of the stage instance.
|
||||||
|
discoverable_disabled: :class:`bool`
|
||||||
|
Whether the stage instance is discoverable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'_state',
|
||||||
|
'id',
|
||||||
|
'guild',
|
||||||
|
'channel_id',
|
||||||
|
'topic',
|
||||||
|
'privacy_level',
|
||||||
|
'discoverable_disabled',
|
||||||
|
'_cs_channel',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, state: ConnectionState, guild: Guild, data: StageInstancePayload) -> None:
|
||||||
|
self._state = state
|
||||||
|
self.guild = guild
|
||||||
|
self._update(data)
|
||||||
|
|
||||||
|
def _update(self, data: StageInstancePayload):
|
||||||
|
self.id: int = int(data['id'])
|
||||||
|
self.channel_id: int = int(data['channel_id'])
|
||||||
|
self.topic: str = data['topic']
|
||||||
|
self.privacy_level = try_enum(StagePrivacyLevel, data['privacy_level'])
|
||||||
|
self.discoverable_disabled = data['discoverable_disabled']
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<StageInstance id={self.id} guild={self.guild!r} channel_id={self.channel_id} topic={self.topic!r}>'
|
||||||
|
|
||||||
|
@cached_slot_property('_cs_channel')
|
||||||
|
def channel(self) -> Optional[StageChannel]:
|
||||||
|
"""Optional[:class:`StageChannel`: The guild that stage instance is running in."""
|
||||||
|
return self._state.get_channel(self.channel_id)
|
||||||
|
|
||||||
|
def is_public(self) -> bool:
|
||||||
|
return self.privacy_level is StagePrivacyLevel.public
|
||||||
|
|
||||||
|
async def edit(self, *, topic: str = MISSING, privacy_level: StagePrivacyLevel = MISSING) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Edits the stage instance.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||||
|
use this.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
topic: :class:`str`
|
||||||
|
The stage instance's new topic.
|
||||||
|
privacy_level: :class:`StagePrivacyLevel`
|
||||||
|
The stage instance's new privacy level.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
InvalidArgument
|
||||||
|
If the ``privacy_level`` parameter is not the proper type.
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to edit the stage instance.
|
||||||
|
HTTPException
|
||||||
|
Editing a stage instance failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
if topic is not MISSING:
|
||||||
|
payload['topic'] = topic
|
||||||
|
|
||||||
|
if privacy_level is not MISSING:
|
||||||
|
if not isinstance(privacy_level, StagePrivacyLevel):
|
||||||
|
raise InvalidArgument('privacy_level field must be of type PrivacyLevel')
|
||||||
|
|
||||||
|
payload['privacy_level'] = privacy_level.value
|
||||||
|
|
||||||
|
if payload:
|
||||||
|
await self._state.http.edit_stage_instance(self.channel_id, **payload)
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Deletes the stage instance.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||||
|
use this.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to delete the stage instance.
|
||||||
|
HTTPException
|
||||||
|
Deleting the stage instance failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.delete_stage_instance(self.channel_id)
|
||||||
299
discord/state.py
299
discord/state.py
@@ -28,6 +28,7 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
import weakref
|
import weakref
|
||||||
import warnings
|
import warnings
|
||||||
import inspect
|
import inspect
|
||||||
@@ -43,15 +44,21 @@ from .mentions import AllowedMentions
|
|||||||
from .partial_emoji import PartialEmoji
|
from .partial_emoji import PartialEmoji
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .channel import *
|
from .channel import *
|
||||||
|
from .channel import _channel_factory
|
||||||
from .raw_models import *
|
from .raw_models import *
|
||||||
from .member import Member
|
from .member import Member
|
||||||
from .role import Role
|
from .role import Role
|
||||||
from .enums import ChannelType, try_enum, Status
|
from .enums import ChannelType, try_enum, Status
|
||||||
from . import utils
|
from . import utils
|
||||||
from .flags import Intents, MemberCacheFlags
|
from .flags import ApplicationFlags, Intents, MemberCacheFlags
|
||||||
from .object import Object
|
from .object import Object
|
||||||
from .invite import Invite
|
from .invite import Invite
|
||||||
|
from .integrations import _integration_factory
|
||||||
from .interactions import Interaction
|
from .interactions import Interaction
|
||||||
|
from .ui.view import ViewStore
|
||||||
|
from .stage_instance import StageInstance
|
||||||
|
from .threads import Thread, ThreadMember
|
||||||
|
from discord import guild
|
||||||
|
|
||||||
class ChunkRequest:
|
class ChunkRequest:
|
||||||
def __init__(self, guild_id, loop, resolver, *, cache=True):
|
def __init__(self, guild_id, loop, resolver, *, cache=True):
|
||||||
@@ -120,7 +127,6 @@ class ConnectionState:
|
|||||||
if self.guild_ready_timeout < 0:
|
if self.guild_ready_timeout < 0:
|
||||||
raise ValueError('guild_ready_timeout cannot be negative')
|
raise ValueError('guild_ready_timeout cannot be negative')
|
||||||
|
|
||||||
self.guild_subscriptions = options.get('guild_subscriptions', True)
|
|
||||||
allowed_mentions = options.get('allowed_mentions')
|
allowed_mentions = options.get('allowed_mentions')
|
||||||
|
|
||||||
if allowed_mentions is not None and not isinstance(allowed_mentions, AllowedMentions):
|
if allowed_mentions is not None and not isinstance(allowed_mentions, AllowedMentions):
|
||||||
@@ -153,15 +159,7 @@ class ConnectionState:
|
|||||||
if not intents.guilds:
|
if not intents.guilds:
|
||||||
log.warning('Guilds intent seems to be disabled. This may cause state related issues.')
|
log.warning('Guilds intent seems to be disabled. This may cause state related issues.')
|
||||||
|
|
||||||
try:
|
self._chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
|
||||||
chunk_guilds = options['fetch_offline_members']
|
|
||||||
except KeyError:
|
|
||||||
chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
|
|
||||||
else:
|
|
||||||
msg = 'fetch_offline_members is deprecated, use chunk_guilds_at_startup instead'
|
|
||||||
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
|
||||||
|
|
||||||
self._chunk_guilds = chunk_guilds
|
|
||||||
|
|
||||||
# Ensure these two are set properly
|
# Ensure these two are set properly
|
||||||
if not intents.members and self._chunk_guilds:
|
if not intents.members and self._chunk_guilds:
|
||||||
@@ -196,6 +194,7 @@ class ConnectionState:
|
|||||||
self._users = weakref.WeakValueDictionary()
|
self._users = weakref.WeakValueDictionary()
|
||||||
self._emojis = {}
|
self._emojis = {}
|
||||||
self._guilds = {}
|
self._guilds = {}
|
||||||
|
self._view_store = ViewStore(self)
|
||||||
self._voice_clients = {}
|
self._voice_clients = {}
|
||||||
|
|
||||||
# LRU of max size 128
|
# LRU of max size 128
|
||||||
@@ -287,6 +286,16 @@ class ConnectionState:
|
|||||||
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
|
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
|
||||||
return emoji
|
return emoji
|
||||||
|
|
||||||
|
def store_view(self, view, message_id=None):
|
||||||
|
self._view_store.add_view(view, message_id)
|
||||||
|
|
||||||
|
def prevent_view_updates_for(self, message_id):
|
||||||
|
return self._view_store.remove_message_tracking(message_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persistent_views(self):
|
||||||
|
return self._view_store.persistent_views
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def guilds(self):
|
def guilds(self):
|
||||||
return list(self._guilds.values())
|
return list(self._guilds.values())
|
||||||
@@ -338,10 +347,10 @@ class ConnectionState:
|
|||||||
|
|
||||||
if len(self._private_channels) > 128:
|
if len(self._private_channels) > 128:
|
||||||
_, to_remove = self._private_channels.popitem(last=False)
|
_, to_remove = self._private_channels.popitem(last=False)
|
||||||
if isinstance(to_remove, DMChannel):
|
if isinstance(to_remove, DMChannel) and to_remove.recipient:
|
||||||
self._private_channels_by_user.pop(to_remove.recipient.id, None)
|
self._private_channels_by_user.pop(to_remove.recipient.id, None)
|
||||||
|
|
||||||
if isinstance(channel, DMChannel):
|
if isinstance(channel, DMChannel) and channel.recipient:
|
||||||
self._private_channels_by_user[channel.recipient.id] = channel
|
self._private_channels_by_user[channel.recipient.id] = channel
|
||||||
|
|
||||||
def add_dm_channel(self, data):
|
def add_dm_channel(self, data):
|
||||||
@@ -371,10 +380,10 @@ class ConnectionState:
|
|||||||
try:
|
try:
|
||||||
guild = self._get_guild(int(data['guild_id']))
|
guild = self._get_guild(int(data['guild_id']))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
channel = self.get_channel(channel_id)
|
channel = DMChannel._from_message(self, channel_id)
|
||||||
guild = None
|
guild = None
|
||||||
else:
|
else:
|
||||||
channel = guild and guild.get_channel(channel_id)
|
channel = guild and (guild.get_channel(channel_id) or guild.get_thread(channel_id))
|
||||||
|
|
||||||
return channel or Object(id=channel_id), guild
|
return channel or Object(id=channel_id), guild
|
||||||
|
|
||||||
@@ -461,6 +470,7 @@ class ConnectionState:
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||||
|
self.application_flags = ApplicationFlags._from_value(application['flags'])
|
||||||
|
|
||||||
for guild_data in data['guilds']:
|
for guild_data in data['guilds']:
|
||||||
self._add_guild_from_data(guild_data)
|
self._add_guild_from_data(guild_data)
|
||||||
@@ -477,7 +487,7 @@ class ConnectionState:
|
|||||||
self.dispatch('message', message)
|
self.dispatch('message', message)
|
||||||
if self._messages is not None:
|
if self._messages is not None:
|
||||||
self._messages.append(message)
|
self._messages.append(message)
|
||||||
if channel and channel.__class__ is TextChannel:
|
if channel and channel.__class__ in (TextChannel, Thread):
|
||||||
channel.last_message_id = message.id
|
channel.last_message_id = message.id
|
||||||
|
|
||||||
def parse_message_delete(self, data):
|
def parse_message_delete(self, data):
|
||||||
@@ -517,6 +527,9 @@ class ConnectionState:
|
|||||||
else:
|
else:
|
||||||
self.dispatch('raw_message_edit', raw)
|
self.dispatch('raw_message_edit', raw)
|
||||||
|
|
||||||
|
if 'components' in data and self._view_store.is_message_tracked(raw.message_id):
|
||||||
|
self._view_store.update_from_message(raw.message_id, data['components'])
|
||||||
|
|
||||||
def parse_message_reaction_add(self, data):
|
def parse_message_reaction_add(self, data):
|
||||||
emoji = data['emoji']
|
emoji = data['emoji']
|
||||||
emoji_id = utils._get_as_snowflake(emoji, 'id')
|
emoji_id = utils._get_as_snowflake(emoji, 'id')
|
||||||
@@ -589,6 +602,11 @@ class ConnectionState:
|
|||||||
|
|
||||||
def parse_interaction_create(self, data):
|
def parse_interaction_create(self, data):
|
||||||
interaction = Interaction(data=data, state=self)
|
interaction = Interaction(data=data, state=self)
|
||||||
|
if data['type'] == 3: # interaction component
|
||||||
|
custom_id = interaction.data['custom_id'] # type: ignore
|
||||||
|
component_type = interaction.data['component_type'] # type: ignore
|
||||||
|
self._view_store.dispatch(component_type, custom_id, interaction)
|
||||||
|
|
||||||
self.dispatch('interaction', interaction)
|
self.dispatch('interaction', interaction)
|
||||||
|
|
||||||
def parse_presence_update(self, data):
|
def parse_presence_update(self, data):
|
||||||
@@ -601,24 +619,14 @@ class ConnectionState:
|
|||||||
user = data['user']
|
user = data['user']
|
||||||
member_id = int(user['id'])
|
member_id = int(user['id'])
|
||||||
member = guild.get_member(member_id)
|
member = guild.get_member(member_id)
|
||||||
flags = self.member_cache_flags
|
|
||||||
if member is None:
|
if member is None:
|
||||||
if 'username' not in user:
|
log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id)
|
||||||
# sometimes we receive 'incomplete' member data post-removal.
|
return
|
||||||
# skip these useless cases.
|
|
||||||
return
|
|
||||||
|
|
||||||
member, old_member = Member._from_presence_update(guild=guild, data=data, state=self)
|
old_member = Member._copy(member)
|
||||||
if flags.online or (flags._online_only and member.raw_status != 'offline'):
|
user_update = member._presence_update(data=data, user=user)
|
||||||
guild._add_member(member)
|
if user_update:
|
||||||
else:
|
self.dispatch('user_update', user_update[0], user_update[1])
|
||||||
old_member = Member._copy(member)
|
|
||||||
user_update = member._presence_update(data=data, user=user)
|
|
||||||
if user_update:
|
|
||||||
self.dispatch('user_update', user_update[0], user_update[1])
|
|
||||||
|
|
||||||
if member.id != self.self_id and flags._online_only and member.raw_status == 'offline':
|
|
||||||
guild._remove_member(member)
|
|
||||||
|
|
||||||
self.dispatch('member_update', old_member, member)
|
self.dispatch('member_update', old_member, member)
|
||||||
|
|
||||||
@@ -641,13 +649,6 @@ class ConnectionState:
|
|||||||
if channel is not None:
|
if channel is not None:
|
||||||
guild._remove_channel(channel)
|
guild._remove_channel(channel)
|
||||||
self.dispatch('guild_channel_delete', channel)
|
self.dispatch('guild_channel_delete', channel)
|
||||||
else:
|
|
||||||
# the reason we're doing this is so it's also removed from the
|
|
||||||
# private channel by user cache as well
|
|
||||||
channel = self._get_private_channel(channel_id)
|
|
||||||
if channel is not None:
|
|
||||||
self._remove_private_channel(channel)
|
|
||||||
self.dispatch('private_channel_delete', channel)
|
|
||||||
|
|
||||||
def parse_channel_update(self, data):
|
def parse_channel_update(self, data):
|
||||||
channel_type = try_enum(ChannelType, data.get('type'))
|
channel_type = try_enum(ChannelType, data.get('type'))
|
||||||
@@ -678,22 +679,15 @@ class ConnectionState:
|
|||||||
log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type'])
|
log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type'])
|
||||||
return
|
return
|
||||||
|
|
||||||
if ch_type in (ChannelType.group, ChannelType.private):
|
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||||
channel_id = int(data['id'])
|
guild = self._get_guild(guild_id)
|
||||||
if self._get_private_channel(channel_id) is None:
|
if guild is not None:
|
||||||
channel = factory(me=self.user, data=data, state=self)
|
channel = factory(guild=guild, state=self, data=data)
|
||||||
self._add_private_channel(channel)
|
guild._add_channel(channel)
|
||||||
self.dispatch('private_channel_create', channel)
|
self.dispatch('guild_channel_create', channel)
|
||||||
else:
|
else:
|
||||||
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
log.debug('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
||||||
guild = self._get_guild(guild_id)
|
return
|
||||||
if guild is not None:
|
|
||||||
channel = factory(guild=guild, state=self, data=data)
|
|
||||||
guild._add_channel(channel)
|
|
||||||
self.dispatch('guild_channel_create', channel)
|
|
||||||
else:
|
|
||||||
log.debug('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
def parse_channel_pins_update(self, data):
|
def parse_channel_pins_update(self, data):
|
||||||
channel_id = int(data['channel_id'])
|
channel_id = int(data['channel_id'])
|
||||||
@@ -714,6 +708,132 @@ class ConnectionState:
|
|||||||
else:
|
else:
|
||||||
self.dispatch('guild_channel_pins_update', channel, last_pin)
|
self.dispatch('guild_channel_pins_update', channel, last_pin)
|
||||||
|
|
||||||
|
def parse_thread_create(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_CREATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread = Thread(guild=guild, data=data)
|
||||||
|
has_thread = guild.get_thread(thread.id)
|
||||||
|
guild._add_thread(thread)
|
||||||
|
if not has_thread:
|
||||||
|
self.dispatch('thread_join', thread)
|
||||||
|
|
||||||
|
def parse_thread_update(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = int(data['id'])
|
||||||
|
thread = guild.get_thread(thread_id)
|
||||||
|
if thread is not None:
|
||||||
|
old = copy.copy(thread)
|
||||||
|
thread._update(data)
|
||||||
|
self.dispatch('thread_update', old, thread)
|
||||||
|
|
||||||
|
def parse_thread_delete(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_DELETE referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = int(data['id'])
|
||||||
|
thread = guild.get_thread(thread_id)
|
||||||
|
if thread is not None:
|
||||||
|
guild._remove_thread(thread)
|
||||||
|
self.dispatch('thread_delete', thread)
|
||||||
|
|
||||||
|
def parse_thread_list_sync(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_LIST_SYNC referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel_ids = set(data['channel_ids'])
|
||||||
|
except KeyError:
|
||||||
|
# If not provided, then the entire guild is being synced
|
||||||
|
# So all previous thread data should be overwritten
|
||||||
|
previous_threads = guild._threads.copy()
|
||||||
|
guild._clear_threads()
|
||||||
|
else:
|
||||||
|
previous_threads = guild._filter_threads(channel_ids)
|
||||||
|
|
||||||
|
threads = {
|
||||||
|
d['id']: guild._store_thread(d)
|
||||||
|
for d in data.get('threads', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
for member in data.get('members', []):
|
||||||
|
try:
|
||||||
|
# note: member['id'] is the thread_id
|
||||||
|
thread = threads[member['id']]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
thread._add_member(ThreadMember(thread, member))
|
||||||
|
|
||||||
|
for thread in threads.values():
|
||||||
|
old = previous_threads.pop(thread.id, None)
|
||||||
|
if old is None:
|
||||||
|
self.dispatch('thread_join', thread)
|
||||||
|
|
||||||
|
for thread in previous_threads.values():
|
||||||
|
self.dispatch('thread_remove', thread)
|
||||||
|
|
||||||
|
def parse_thread_member_update(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = int(data['id'])
|
||||||
|
thread: Optional[Thread] = guild.get_thread(thread_id)
|
||||||
|
if thread is None:
|
||||||
|
log.debug('THREAD_MEMBER_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
member = ThreadMember(thread, data)
|
||||||
|
thread.me = member
|
||||||
|
|
||||||
|
def parse_thread_members_update(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
log.debug('THREAD_MEMBERS_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = int(data['id'])
|
||||||
|
thread: Optional[Thread] = guild.get_thread(thread_id)
|
||||||
|
if thread is None:
|
||||||
|
log.debug('THREAD_MEMBERS_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
added_members = [ThreadMember(thread, d) for d in data.get('added_members', [])]
|
||||||
|
removed_member_ids = data.get('removed_member_ids', [])
|
||||||
|
self_id = self.self_id
|
||||||
|
for member in added_members:
|
||||||
|
if member.id != self_id:
|
||||||
|
thread._add_member(member)
|
||||||
|
self.dispatch('thread_member_join', member)
|
||||||
|
else:
|
||||||
|
thread.me = member
|
||||||
|
self.dispatch('thread_join', thread)
|
||||||
|
|
||||||
|
for member_id in removed_member_ids:
|
||||||
|
if member_id != self_id:
|
||||||
|
member = thread._pop_member(member_id)
|
||||||
|
self.dispatch('thread_member_leave', member)
|
||||||
|
else:
|
||||||
|
self.dispatch('thread_remove', thread)
|
||||||
|
|
||||||
def parse_guild_member_add(self, data):
|
def parse_guild_member_add(self, data):
|
||||||
guild = self._get_guild(int(data['guild_id']))
|
guild = self._get_guild(int(data['guild_id']))
|
||||||
if guild is None:
|
if guild is None:
|
||||||
@@ -801,6 +921,9 @@ class ConnectionState:
|
|||||||
|
|
||||||
return self._add_guild_from_data(data)
|
return self._add_guild_from_data(data)
|
||||||
|
|
||||||
|
def is_guild_evicted(self, guild) -> bool:
|
||||||
|
return guild.id not in self._guilds
|
||||||
|
|
||||||
async def chunk_guild(self, guild, *, wait=True, cache=None):
|
async def chunk_guild(self, guild, *, wait=True, cache=None):
|
||||||
cache = cache or self.member_cache_flags.joined
|
cache = cache or self.member_cache_flags.joined
|
||||||
request = self._chunk_requests.get(guild.id)
|
request = self._chunk_requests.get(guild.id)
|
||||||
@@ -965,6 +1088,35 @@ class ConnectionState:
|
|||||||
else:
|
else:
|
||||||
log.debug('GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
log.debug('GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
||||||
|
|
||||||
|
def parse_integration_create(self, data):
|
||||||
|
guild_id = int(data.pop('guild_id'))
|
||||||
|
guild = self._get_guild(guild_id)
|
||||||
|
if guild is not None:
|
||||||
|
cls, _ = _integration_factory(data['type'])
|
||||||
|
integration = cls(data=data, guild=guild)
|
||||||
|
self.dispatch('integration_create', integration)
|
||||||
|
else:
|
||||||
|
log.debug('INTEGRATION_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
||||||
|
|
||||||
|
def parse_integration_update(self, data):
|
||||||
|
guild_id = int(data.pop('guild_id'))
|
||||||
|
guild = self._get_guild(guild_id)
|
||||||
|
if guild is not None:
|
||||||
|
cls, _ = _integration_factory(data['type'])
|
||||||
|
integration = cls(data=data, guild=guild)
|
||||||
|
self.dispatch('integration_update', integration)
|
||||||
|
else:
|
||||||
|
log.debug('INTEGRATION_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
||||||
|
|
||||||
|
def parse_integration_delete(self, data):
|
||||||
|
guild_id = int(data['guild_id'])
|
||||||
|
guild = self._get_guild(guild_id)
|
||||||
|
if guild is not None:
|
||||||
|
raw = RawIntegrationDeleteEvent(data)
|
||||||
|
self.dispatch('raw_integration_delete', raw)
|
||||||
|
else:
|
||||||
|
log.debug('INTEGRATION_DELETE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
||||||
|
|
||||||
def parse_webhooks_update(self, data):
|
def parse_webhooks_update(self, data):
|
||||||
channel = self.get_channel(int(data['channel_id']))
|
channel = self.get_channel(int(data['channel_id']))
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
@@ -972,6 +1124,40 @@ class ConnectionState:
|
|||||||
else:
|
else:
|
||||||
log.debug('WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.', data['channel_id'])
|
log.debug('WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.', data['channel_id'])
|
||||||
|
|
||||||
|
def parse_stage_instance_create(self, data):
|
||||||
|
guild = self._get_guild(int(data['guild_id']))
|
||||||
|
if guild is not None:
|
||||||
|
stage_instance = StageInstance(guild=guild, state=self, data=data)
|
||||||
|
guild._stage_instances[stage_instance.id] = stage_instance
|
||||||
|
self.dispatch('stage_instance_create', stage_instance)
|
||||||
|
else:
|
||||||
|
log.debug('STAGE_INSTANCE_CREATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
||||||
|
|
||||||
|
def parse_stage_instance_update(self, data):
|
||||||
|
guild = self._get_guild(int(data['guild_id']))
|
||||||
|
if guild is not None:
|
||||||
|
stage_instance = guild._stage_instances.get(int(data['id']))
|
||||||
|
if stage_instance is not None:
|
||||||
|
old_stage_instance = copy.copy(stage_instance)
|
||||||
|
stage_instance._update(data)
|
||||||
|
self.dispatch('stage_instance_update', old_stage_instance, stage_instance)
|
||||||
|
else:
|
||||||
|
log.debug('STAGE_INSTANCE_UPDATE referencing unknown stage instance ID: %s. Discarding.', data['id'])
|
||||||
|
else:
|
||||||
|
log.debug('STAGE_INSTANCE_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
||||||
|
|
||||||
|
def parse_stage_instance_delete(self, data):
|
||||||
|
guild = self._get_guild(int(data['guild_id']))
|
||||||
|
if guild is not None:
|
||||||
|
try:
|
||||||
|
stage_instance = guild._stage_instances.pop(int(data['id']))
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.dispatch('stage_instance_delete', stage_instance)
|
||||||
|
else:
|
||||||
|
log.debug('STAGE_INSTANCE_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
||||||
|
|
||||||
def parse_voice_state_update(self, data):
|
def parse_voice_state_update(self, data):
|
||||||
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
|
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
|
||||||
channel_id = utils._get_as_snowflake(data, 'channel_id')
|
channel_id = utils._get_as_snowflake(data, 'channel_id')
|
||||||
@@ -1064,7 +1250,7 @@ class ConnectionState:
|
|||||||
return pm
|
return pm
|
||||||
|
|
||||||
for guild in self.guilds:
|
for guild in self.guilds:
|
||||||
channel = guild.get_channel(id)
|
channel = guild.get_channel(id) or guild.get_thread(id)
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
@@ -1086,7 +1272,7 @@ class AutoShardedConnectionState(ConnectionState):
|
|||||||
new_guild = self._get_guild(msg.guild.id)
|
new_guild = self._get_guild(msg.guild.id)
|
||||||
if new_guild is not None and new_guild is not msg.guild:
|
if new_guild is not None and new_guild is not msg.guild:
|
||||||
channel_id = msg.channel.id
|
channel_id = msg.channel.id
|
||||||
channel = new_guild.get_channel(channel_id) or Object(id=channel_id)
|
channel = new_guild.get_channel(channel_id) or new_guild.get_thread(channel_id) or Object(id=channel_id)
|
||||||
msg._rebind_channel_reference(channel)
|
msg._rebind_channel_reference(channel)
|
||||||
|
|
||||||
async def chunker(self, guild_id, query='', limit=0, presences=False, *, shard_id=None, nonce=None):
|
async def chunker(self, guild_id, query='', limit=0, presences=False, *, shard_id=None, nonce=None):
|
||||||
@@ -1174,6 +1360,7 @@ class AutoShardedConnectionState(ConnectionState):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.application_id = utils._get_as_snowflake(application, 'id')
|
self.application_id = utils._get_as_snowflake(application, 'id')
|
||||||
|
self.application_flags = ApplicationFlags._from_value(application['flags'])
|
||||||
|
|
||||||
for guild_data in data['guilds']:
|
for guild_data in data['guilds']:
|
||||||
self._add_guild_from_data(guild_data)
|
self._add_guild_from_data(guild_data)
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
from .mixins import Hashable
|
from .mixins import Hashable
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .utils import snowflake_time
|
from .utils import snowflake_time
|
||||||
@@ -31,8 +34,14 @@ __all__ = (
|
|||||||
'Sticker',
|
'Sticker',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .types.message import Sticker as StickerPayload
|
||||||
|
|
||||||
|
|
||||||
class Sticker(Hashable):
|
class Sticker(Hashable):
|
||||||
"""Represents a sticker
|
"""Represents a sticker.
|
||||||
|
|
||||||
.. versionadded:: 1.6
|
.. versionadded:: 1.6
|
||||||
|
|
||||||
@@ -40,102 +49,72 @@ class Sticker(Hashable):
|
|||||||
|
|
||||||
.. describe:: str(x)
|
.. describe:: str(x)
|
||||||
|
|
||||||
Returns the name of the sticker
|
Returns the name of the sticker.
|
||||||
|
|
||||||
.. describe:: x == y
|
.. describe:: x == y
|
||||||
|
|
||||||
Checks if the sticker is equal to another sticker
|
Checks if the sticker is equal to another sticker.
|
||||||
|
|
||||||
.. describe:: x != y
|
.. describe:: x != y
|
||||||
|
|
||||||
Checks if the sticker is not equal to another sticker
|
Checks if the sticker is not equal to another sticker.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The sticker's name
|
The sticker's name.
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
The id of the sticker
|
The id of the sticker.
|
||||||
description: :class:`str`
|
description: :class:`str`
|
||||||
The description of the sticker
|
The description of the sticker.
|
||||||
pack_id: :class:`int`
|
pack_id: :class:`int`
|
||||||
The id of the sticker's pack
|
The id of the sticker's pack.
|
||||||
format: :class:`StickerType`
|
format: :class:`StickerType`
|
||||||
The format for the sticker's image
|
The format for the sticker's image.
|
||||||
image: :class:`str`
|
|
||||||
The sticker's image
|
|
||||||
tags: List[:class:`str`]
|
tags: List[:class:`str`]
|
||||||
A list of tags for the sticker
|
A list of tags for the sticker.
|
||||||
preview_image: Optional[:class:`str`]
|
|
||||||
The sticker's preview asset hash
|
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags', 'preview_image')
|
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', '_image', 'tags')
|
||||||
self._state = state
|
|
||||||
self.id = int(data['id'])
|
def __init__(self, *, state: ConnectionState, data: StickerPayload):
|
||||||
self.name = data['name']
|
self._state: ConnectionState = state
|
||||||
self.description = data['description']
|
self.id: int = int(data['id'])
|
||||||
self.pack_id = int(data['pack_id'])
|
self.name: str = data['name']
|
||||||
self.format = try_enum(StickerType, data['format_type'])
|
self.description: str = data['description']
|
||||||
self.image = data['asset']
|
self.pack_id: int = int(data['pack_id'])
|
||||||
|
self.format: StickerType = try_enum(StickerType, data['format_type'])
|
||||||
|
self._image: str = data['asset']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.tags = [tag.strip() for tag in data['tags'].split(',')]
|
self.tags: List[str] = [tag.strip() for tag in data['tags'].split(',')]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.tags = []
|
self.tags = []
|
||||||
|
|
||||||
self.preview_image = data.get('preview_asset')
|
def __repr__(self) -> str:
|
||||||
|
return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>'
|
||||||
|
|
||||||
def __repr__(self):
|
def __str__(self) -> str:
|
||||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r}>'.format(self)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image_url(self):
|
def image(self) -> Optional[Asset]:
|
||||||
"""Returns an :class:`Asset` for the sticker's image.
|
"""Returns an :class:`Asset` for the sticker's image.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This will return ``None`` if the format is ``StickerType.lottie``
|
This will return ``None`` if the format is ``StickerType.lottie``.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Optional[:class:`Asset`]
|
Optional[:class:`Asset`]
|
||||||
The resulting CDN asset.
|
The resulting CDN asset.
|
||||||
"""
|
"""
|
||||||
return self.image_url_as()
|
|
||||||
|
|
||||||
def image_url_as(self, *, size=1024):
|
|
||||||
"""Optionally returns an :class:`Asset` for the sticker's image.
|
|
||||||
|
|
||||||
The size must be a power of 2 between 16 and 4096.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This will return ``None`` if the format is ``StickerType.lottie``.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Optional[:class:`Asset`]
|
|
||||||
The resulting CDN asset or ``None``.
|
|
||||||
"""
|
|
||||||
if self.format is StickerType.lottie:
|
if self.format is StickerType.lottie:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Asset._from_sticker_url(self._state, self, size=size)
|
return Asset._from_sticker(self._state, self.id, self._image)
|
||||||
|
|||||||
@@ -22,16 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .user import BaseUser
|
from .user import BaseUser
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .enums import TeamMembershipState, try_enum
|
from .enums import TeamMembershipState, try_enum
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional, List
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .state import ConnectionState
|
||||||
|
|
||||||
|
from .types.team import (
|
||||||
|
Team as TeamPayload,
|
||||||
|
TeamMember as TeamMemberPayload,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Team',
|
'Team',
|
||||||
'TeamMember',
|
'TeamMember',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Team:
|
class Team:
|
||||||
"""Represents an application team for a bot provided by Discord.
|
"""Represents an application team for a bot provided by Discord.
|
||||||
|
|
||||||
@@ -41,8 +54,6 @@ class Team:
|
|||||||
The team ID.
|
The team ID.
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The team name
|
The team name
|
||||||
icon: Optional[:class:`str`]
|
|
||||||
The icon hash, if it exists.
|
|
||||||
owner_id: :class:`int`
|
owner_id: :class:`int`
|
||||||
The team's owner ID.
|
The team's owner ID.
|
||||||
members: List[:class:`TeamMember`]
|
members: List[:class:`TeamMember`]
|
||||||
@@ -50,61 +61,34 @@ class Team:
|
|||||||
|
|
||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members')
|
|
||||||
|
|
||||||
def __init__(self, state, data):
|
__slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members')
|
||||||
self._state = state
|
|
||||||
|
|
||||||
self.id = utils._get_as_snowflake(data, 'id')
|
def __init__(self, state: ConnectionState, data: TeamPayload):
|
||||||
self.name = data['name']
|
self._state: ConnectionState = state
|
||||||
self.icon = data['icon']
|
|
||||||
self.owner_id = utils._get_as_snowflake(data, 'owner_user_id')
|
|
||||||
self.members = [TeamMember(self, self._state, member) for member in data['members']]
|
|
||||||
|
|
||||||
def __repr__(self):
|
self.id: int = int(data['id'])
|
||||||
return '<{0.__class__.__name__} id={0.id} name={0.name}>'.format(self)
|
self.name: str = data['name']
|
||||||
|
self._icon: Optional[str] = data['icon']
|
||||||
|
self.owner_id: Optional[int] = utils._get_as_snowflake(data, 'owner_user_id')
|
||||||
|
self.members: List[TeamMember] = [TeamMember(self, self._state, member) for member in data['members']]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<{self.__class__.__name__} id={self.id} name={self.name}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon_url(self):
|
def icon(self) -> Optional[Asset]:
|
||||||
""":class:`.Asset`: Retrieves the team's icon asset.
|
"""Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any."""
|
||||||
|
if self._icon is None:
|
||||||
This is equivalent to calling :meth:`icon_url_as` with
|
return None
|
||||||
the default parameters ('webp' format and a size of 1024).
|
return Asset._from_icon(self._state, self.id, self._icon, path='team')
|
||||||
"""
|
|
||||||
return self.icon_url_as()
|
|
||||||
|
|
||||||
def icon_url_as(self, *, format='webp', size=1024):
|
|
||||||
"""Returns an :class:`Asset` for the icon the team has.
|
|
||||||
|
|
||||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
|
||||||
The size must be a power of 2 between 16 and 4096.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
format: :class:`str`
|
|
||||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_icon(self._state, self, 'team', format=format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self):
|
def owner(self) -> Optional[TeamMember]:
|
||||||
"""Optional[:class:`TeamMember`]: The team's owner."""
|
"""Optional[:class:`TeamMember`]: The team's owner."""
|
||||||
return utils.get(self.members, id=self.owner_id)
|
return utils.get(self.members, id=self.owner_id)
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(BaseUser):
|
class TeamMember(BaseUser):
|
||||||
"""Represents a team member in a team.
|
"""Represents a team member in a team.
|
||||||
|
|
||||||
@@ -145,14 +129,17 @@ class TeamMember(BaseUser):
|
|||||||
membership_state: :class:`TeamMembershipState`
|
membership_state: :class:`TeamMembershipState`
|
||||||
The membership state of the member (e.g. invited or accepted)
|
The membership state of the member (e.g. invited or accepted)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = BaseUser.__slots__ + ('team', 'membership_state', 'permissions')
|
__slots__ = BaseUser.__slots__ + ('team', 'membership_state', 'permissions')
|
||||||
|
|
||||||
def __init__(self, team, state, data):
|
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload):
|
||||||
self.team = team
|
self.team: Team = team
|
||||||
self.membership_state = try_enum(TeamMembershipState, data['membership_state'])
|
self.membership_state: TeamMembershipState = try_enum(TeamMembershipState, data['membership_state'])
|
||||||
self.permissions = data['permissions']
|
self.permissions: List[str] = data['permissions']
|
||||||
super().__init__(state=state, data=data['user'])
|
super().__init__(state=state, data=data['user'])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} ' \
|
return (
|
||||||
'discriminator={0.discriminator!r} membership_state={0.membership_state!r}>'.format(self)
|
f'<{self.__class__.__name__} id={self.id} name={self.name!r} '
|
||||||
|
f'discriminator={self.discriminator!r} membership_state={self.membership_state!r}>'
|
||||||
|
)
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional, TYPE_CHECKING, overload
|
||||||
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data
|
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data
|
||||||
from .enums import VoiceRegion
|
from .enums import VoiceRegion
|
||||||
from .guild import Guild
|
from .guild import Guild
|
||||||
@@ -30,12 +33,17 @@ __all__ = (
|
|||||||
'Template',
|
'Template',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.template import Template as TemplatePayload
|
||||||
|
|
||||||
|
|
||||||
class _FriendlyHttpAttributeErrorHelper:
|
class _FriendlyHttpAttributeErrorHelper:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
raise AttributeError('PartialTemplateState does not support http methods.')
|
raise AttributeError('PartialTemplateState does not support http methods.')
|
||||||
|
|
||||||
|
|
||||||
class _PartialTemplateState:
|
class _PartialTemplateState:
|
||||||
def __init__(self, *, state):
|
def __init__(self, *, state):
|
||||||
self.__state = state
|
self.__state = state
|
||||||
@@ -72,6 +80,7 @@ class _PartialTemplateState:
|
|||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
|
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
|
||||||
|
|
||||||
|
|
||||||
class Template:
|
class Template:
|
||||||
"""Represents a Discord template.
|
"""Represents a Discord template.
|
||||||
|
|
||||||
@@ -96,16 +105,33 @@ class Template:
|
|||||||
This is referred to as "last synced" in the official Discord client.
|
This is referred to as "last synced" in the official Discord client.
|
||||||
source_guild: :class:`Guild`
|
source_guild: :class:`Guild`
|
||||||
The source guild.
|
The source guild.
|
||||||
|
is_dirty: Optional[:class:`bool`]
|
||||||
|
Whether the template has unsynced changes.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
__slots__ = (
|
||||||
|
'code',
|
||||||
|
'uses',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'creator',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'source_guild',
|
||||||
|
'is_dirty',
|
||||||
|
'_state',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, state, data: TemplatePayload):
|
||||||
self._state = state
|
self._state = state
|
||||||
self._store(data)
|
self._store(data)
|
||||||
|
|
||||||
def _store(self, data):
|
def _store(self, data: TemplatePayload):
|
||||||
self.code = data['code']
|
self.code = data['code']
|
||||||
self.uses = data['usage_count']
|
self.uses = data['usage_count']
|
||||||
self.name = data['name']
|
self.name = data['name']
|
||||||
self.description = data['description']
|
self.description = data['description']
|
||||||
creator_data = data.get('creator')
|
creator_data = data.get('creator')
|
||||||
self.creator = None if creator_data is None else self._state.store_user(creator_data)
|
self.creator = None if creator_data is None else self._state.store_user(creator_data)
|
||||||
@@ -117,19 +143,22 @@ class Template:
|
|||||||
|
|
||||||
guild = self._state._get_guild(id)
|
guild = self._state._get_guild(id)
|
||||||
|
|
||||||
if guild is None:
|
if guild is None and id:
|
||||||
source_serialised = data['serialized_source_guild']
|
source_serialised = data['serialized_source_guild']
|
||||||
source_serialised['id'] = id
|
source_serialised['id'] = id
|
||||||
state = _PartialTemplateState(state=self._state)
|
state = _PartialTemplateState(state=self._state)
|
||||||
guild = Guild(data=source_serialised, state=state)
|
guild = Guild(data=source_serialised, state=state)
|
||||||
|
|
||||||
self.source_guild = guild
|
self.source_guild = guild
|
||||||
|
self.is_dirty = data.get('is_dirty', None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Template code={0.code!r} uses={0.uses} name={0.name!r}' \
|
return (
|
||||||
' creator={0.creator!r} source_guild={0.source_guild!r}>'.format(self)
|
f'<Template code={self.code!r} uses={self.uses} name={self.name!r}'
|
||||||
|
f' creator={self.creator!r} source_guild={self.source_guild!r} is_dirty={self.is_dirty}>'
|
||||||
|
)
|
||||||
|
|
||||||
async def create_guild(self, name, region=None, icon=None):
|
async def create_guild(self, name: str, region: Optional[VoiceRegion] = None, icon: Any = None):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Creates a :class:`.Guild` using the template.
|
Creates a :class:`.Guild` using the template.
|
||||||
@@ -169,7 +198,7 @@ class Template:
|
|||||||
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
|
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
|
||||||
return Guild(data=data, state=self._state)
|
return Guild(data=data, state=self._state)
|
||||||
|
|
||||||
async def sync(self):
|
async def sync(self) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Sync the template to the guild's current state.
|
Sync the template to the guild's current state.
|
||||||
@@ -192,7 +221,20 @@ class Template:
|
|||||||
data = await self._state.http.sync_template(self.source_guild.id, self.code)
|
data = await self._state.http.sync_template(self.source_guild.id, self.code)
|
||||||
self._store(data)
|
self._store(data)
|
||||||
|
|
||||||
async def edit(self, **kwargs):
|
@overload
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: Optional[str] = ...,
|
||||||
|
description: Optional[str] = ...,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def edit(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def edit(self, **kwargs) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Edit the template metadata.
|
Edit the template metadata.
|
||||||
@@ -221,7 +263,7 @@ class Template:
|
|||||||
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
|
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
|
||||||
self._store(data)
|
self._store(data)
|
||||||
|
|
||||||
async def delete(self):
|
async def delete(self) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Delete the template.
|
Delete the template.
|
||||||
@@ -241,3 +283,11 @@ class Template:
|
|||||||
This template does not exist.
|
This template does not exist.
|
||||||
"""
|
"""
|
||||||
await self._state.http.delete_template(self.source_guild.id, self.code)
|
await self._state.http.delete_template(self.source_guild.id, self.code)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
""":class:`str`: The template url.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return f'https://discord.new/{self.code}'
|
||||||
|
|||||||
686
discord/threads.py
Normal file
686
discord/threads.py
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Callable, Dict, Iterable, List, Optional, Union, TYPE_CHECKING
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from .mixins import Hashable
|
||||||
|
from .abc import Messageable
|
||||||
|
from .enums import ChannelType, try_enum
|
||||||
|
from .errors import ClientException
|
||||||
|
from .utils import MISSING, parse_time, _get_as_snowflake
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Thread',
|
||||||
|
'ThreadMember',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .types.threads import (
|
||||||
|
Thread as ThreadPayload,
|
||||||
|
ThreadMember as ThreadMemberPayload,
|
||||||
|
ThreadMetadata,
|
||||||
|
ThreadArchiveDuration,
|
||||||
|
)
|
||||||
|
from .guild import Guild
|
||||||
|
from .channel import TextChannel
|
||||||
|
from .member import Member
|
||||||
|
from .message import Message
|
||||||
|
from .abc import Snowflake, SnowflakeTime
|
||||||
|
from .role import Role
|
||||||
|
from .permissions import Permissions
|
||||||
|
from .state import ConnectionState
|
||||||
|
|
||||||
|
|
||||||
|
class Thread(Messageable, Hashable):
|
||||||
|
"""Represents a Discord thread.
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two threads are equal.
|
||||||
|
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two threads are not equal.
|
||||||
|
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Returns the thread's hash.
|
||||||
|
|
||||||
|
.. describe:: str(x)
|
||||||
|
|
||||||
|
Returns the thread's name.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
name: :class:`str`
|
||||||
|
The thread name.
|
||||||
|
guild: :class:`Guild`
|
||||||
|
The guild the thread belongs to.
|
||||||
|
id: :class:`int`
|
||||||
|
The thread ID.
|
||||||
|
parent_id: :class:`int`
|
||||||
|
The parent :class:`TextChannel` ID this thread belongs to.
|
||||||
|
owner_id: :class:`int`
|
||||||
|
The user's ID that created this thread.
|
||||||
|
last_message_id: Optional[:class:`int`]
|
||||||
|
The last message ID of the message sent to this thread. It may
|
||||||
|
*not* point to an existing or valid message.
|
||||||
|
slowmode_delay: :class:`int`
|
||||||
|
The number of seconds a member must wait between sending messages
|
||||||
|
in this thread. A value of `0` denotes that it is disabled.
|
||||||
|
Bots and users with :attr:`~Permissions.manage_channels` or
|
||||||
|
:attr:`~Permissions.manage_messages` bypass slowmode.
|
||||||
|
message_count: :class:`int`
|
||||||
|
An approximate number of messages in this thread. This caps at 50.
|
||||||
|
member_count: :class:`int`
|
||||||
|
An approximate number of members in this thread. This caps at 50.
|
||||||
|
me: Optional[:class:`ThreadMember`]
|
||||||
|
A thread member representing yourself, if you've joined the thread.
|
||||||
|
This could not be available.
|
||||||
|
archived: :class:`bool`
|
||||||
|
Whether the thread is archived.
|
||||||
|
locked: :class:`bool`
|
||||||
|
Whether the thread is locked.
|
||||||
|
archiver_id: Optional[:class:`int`]
|
||||||
|
The user's ID that archived this thread.
|
||||||
|
auto_archive_duration: :class:`int`
|
||||||
|
The duration in minutes until the thread is automatically archived due to inactivity.
|
||||||
|
Usually a value of 60, 1440, 4320 and 10080.
|
||||||
|
archive_timestamp: :class:`datetime.datetime`
|
||||||
|
An aware timestamp of when the thread's archived status was last updated in UTC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'name',
|
||||||
|
'id',
|
||||||
|
'guild',
|
||||||
|
'_type',
|
||||||
|
'_state',
|
||||||
|
'_members',
|
||||||
|
'owner_id',
|
||||||
|
'parent_id',
|
||||||
|
'last_message_id',
|
||||||
|
'message_count',
|
||||||
|
'member_count',
|
||||||
|
'slowmode_delay',
|
||||||
|
'me',
|
||||||
|
'locked',
|
||||||
|
'archived',
|
||||||
|
'archiver_id',
|
||||||
|
'auto_archive_duration',
|
||||||
|
'archive_timestamp',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *, guild: Guild, data: ThreadPayload):
|
||||||
|
self._state: ConnectionState = guild._state
|
||||||
|
self.guild = guild
|
||||||
|
self._members: Dict[int, ThreadMember] = {}
|
||||||
|
self._from_data(data)
|
||||||
|
|
||||||
|
async def _get_channel(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'<Thread id={self.id!r} name={self.name!r} parent={self.parent}'
|
||||||
|
f' owner_id={self.owner_id!r} locked={self.locked} archived={self.archived}>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def _from_data(self, data: ThreadPayload):
|
||||||
|
self.id = int(data['id'])
|
||||||
|
self.parent_id = int(data['parent_id'])
|
||||||
|
self.owner_id = int(data['owner_id'])
|
||||||
|
self.name = data['name']
|
||||||
|
self._type = try_enum(ChannelType, data['type'])
|
||||||
|
self.last_message_id = _get_as_snowflake(data, 'last_message_id')
|
||||||
|
self.slowmode_delay = data.get('rate_limit_per_user', 0)
|
||||||
|
self._unroll_metadata(data['thread_metadata'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
member = data['member']
|
||||||
|
except KeyError:
|
||||||
|
self.me = None
|
||||||
|
else:
|
||||||
|
self.me = ThreadMember(self, member)
|
||||||
|
|
||||||
|
def _unroll_metadata(self, data: ThreadMetadata):
|
||||||
|
self.archived = data['archived']
|
||||||
|
self.archiver_id = _get_as_snowflake(data, 'archiver_id')
|
||||||
|
self.auto_archive_duration = data['auto_archive_duration']
|
||||||
|
self.archive_timestamp = parse_time(data['archive_timestamp'])
|
||||||
|
self.locked = data.get('locked', False)
|
||||||
|
|
||||||
|
def _update(self, data):
|
||||||
|
try:
|
||||||
|
self.name = data['name']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._unroll_metadata(data['thread_metadata'])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self) -> Optional[TextChannel]:
|
||||||
|
"""Optional[:class:`TextChannel`]: The parent channel this thread belongs to."""
|
||||||
|
return self.guild.get_channel(self.parent_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self) -> Optional[Member]:
|
||||||
|
"""Optional[:class:`Member`]: The member this thread belongs to."""
|
||||||
|
return self.guild.get_member(self.owner_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_message(self) -> Optional[Message]:
|
||||||
|
"""Fetches the last message from this channel in cache.
|
||||||
|
|
||||||
|
The message might not be valid or point to an existing message.
|
||||||
|
|
||||||
|
.. admonition:: Reliable Fetching
|
||||||
|
:class: helpful
|
||||||
|
|
||||||
|
For a slightly more reliable method of fetching the
|
||||||
|
last message, consider using either :meth:`history`
|
||||||
|
or :meth:`fetch_message` with the :attr:`last_message_id`
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
---------
|
||||||
|
Optional[:class:`Message`]
|
||||||
|
The last message in this channel or ``None`` if not found.
|
||||||
|
"""
|
||||||
|
return self._state._get_message(self.last_message_id) if self.last_message_id else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category_id(self) -> Optional[int]:
|
||||||
|
"""The category channel ID the parent channel belongs to, if applicable.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ClientException
|
||||||
|
The parent channel was not cached and returned ``None``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[:class:`int`]
|
||||||
|
The parent channel's category ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parent = self.parent
|
||||||
|
if parent is None:
|
||||||
|
raise ClientException('Parent channel not found')
|
||||||
|
return parent.category_id
|
||||||
|
|
||||||
|
def is_private(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the thread is a private thread.
|
||||||
|
|
||||||
|
A private thread is only viewable by those that have been explicitly
|
||||||
|
invited or have :attr:`~.Permissions.manage_threads`.
|
||||||
|
"""
|
||||||
|
return self._type is ChannelType.private_thread
|
||||||
|
|
||||||
|
def is_news(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the thread is a news thread.
|
||||||
|
|
||||||
|
A news thread is a thread that has a parent that is a news channel,
|
||||||
|
i.e. :meth:`.TextChannel.is_news` is ``True``.
|
||||||
|
"""
|
||||||
|
return self._type is ChannelType.news_thread
|
||||||
|
|
||||||
|
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
|
||||||
|
"""Handles permission resolution for the :class:`~discord.Member`
|
||||||
|
or :class:`~discord.Role`.
|
||||||
|
|
||||||
|
Since threads do not have their own permissions, they inherit them
|
||||||
|
from the parent channel. This is a convenience method for
|
||||||
|
calling :meth:`~discord.TextChannel.permissions_for` on the
|
||||||
|
parent channel.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
obj: Union[:class:`~discord.Member`, :class:`~discord.Role`]
|
||||||
|
The object to resolve permissions for. This could be either
|
||||||
|
a member or a role. If it's a role then member overwrites
|
||||||
|
are not computed.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ClientException
|
||||||
|
The parent channel was not cached and returned ``None``
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`~discord.Permissions`
|
||||||
|
The resolved permissions for the member or role.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parent = self.parent
|
||||||
|
if parent is None:
|
||||||
|
raise ClientException('Parent channel not found')
|
||||||
|
return parent.permissions_for(obj)
|
||||||
|
|
||||||
|
async def delete_messages(self, messages: Iterable[Snowflake]) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Deletes a list of messages. This is similar to :meth:`Message.delete`
|
||||||
|
except it bulk deletes multiple messages.
|
||||||
|
|
||||||
|
As a special case, if the number of messages is 0, then nothing
|
||||||
|
is done. If the number of messages is 1 then single message
|
||||||
|
delete is done. If it's more than two, then bulk delete is used.
|
||||||
|
|
||||||
|
You cannot bulk delete more than 100 messages or messages that
|
||||||
|
are older than 14 days old.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||||
|
use this.
|
||||||
|
|
||||||
|
Usable only by bot accounts.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
messages: Iterable[:class:`abc.Snowflake`]
|
||||||
|
An iterable of messages denoting which ones to bulk delete.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ClientException
|
||||||
|
The number of messages to delete was more than 100.
|
||||||
|
Forbidden
|
||||||
|
You do not have proper permissions to delete the messages or
|
||||||
|
you're not using a bot account.
|
||||||
|
NotFound
|
||||||
|
If single delete, then the message was already deleted.
|
||||||
|
HTTPException
|
||||||
|
Deleting the messages failed.
|
||||||
|
"""
|
||||||
|
if not isinstance(messages, (list, tuple)):
|
||||||
|
messages = list(messages)
|
||||||
|
|
||||||
|
if len(messages) == 0:
|
||||||
|
return # do nothing
|
||||||
|
|
||||||
|
if len(messages) == 1:
|
||||||
|
message_id = messages[0].id
|
||||||
|
await self._state.http.delete_message(self.id, message_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(messages) > 100:
|
||||||
|
raise ClientException('Can only bulk delete messages up to 100 messages')
|
||||||
|
|
||||||
|
message_ids = [m.id for m in messages]
|
||||||
|
await self._state.http.delete_messages(self.id, message_ids)
|
||||||
|
|
||||||
|
async def purge(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
limit: int = 100,
|
||||||
|
check: Callable[[Message], bool] = MISSING,
|
||||||
|
before: Optional[SnowflakeTime] = None,
|
||||||
|
after: Optional[SnowflakeTime] = None,
|
||||||
|
around: Optional[SnowflakeTime] = None,
|
||||||
|
oldest_first: Optional[bool] = False,
|
||||||
|
bulk: bool = True,
|
||||||
|
) -> List[Message]:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Purges a list of messages that meet the criteria given by the predicate
|
||||||
|
``check``. If a ``check`` is not provided then all messages are deleted
|
||||||
|
without discrimination.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||||
|
delete messages even if they are your own (unless you are a user
|
||||||
|
account). The :attr:`~Permissions.read_message_history` permission is
|
||||||
|
also needed to retrieve message history.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
---------
|
||||||
|
|
||||||
|
Deleting bot's messages ::
|
||||||
|
|
||||||
|
def is_me(m):
|
||||||
|
return m.author == client.user
|
||||||
|
|
||||||
|
deleted = await thread.purge(limit=100, check=is_me)
|
||||||
|
await thread.send(f'Deleted {len(deleted)} message(s)')
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
limit: Optional[:class:`int`]
|
||||||
|
The number of messages to search through. This is not the number
|
||||||
|
of messages that will be deleted, though it can be.
|
||||||
|
check: Callable[[:class:`Message`], :class:`bool`]
|
||||||
|
The function used to check if a message should be deleted.
|
||||||
|
It must take a :class:`Message` as its sole parameter.
|
||||||
|
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||||
|
Same as ``before`` in :meth:`history`.
|
||||||
|
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||||
|
Same as ``after`` in :meth:`history`.
|
||||||
|
around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||||
|
Same as ``around`` in :meth:`history`.
|
||||||
|
oldest_first: Optional[:class:`bool`]
|
||||||
|
Same as ``oldest_first`` in :meth:`history`.
|
||||||
|
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 messages are older than two weeks.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have proper permissions to do the actions required.
|
||||||
|
HTTPException
|
||||||
|
Purging the messages failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
List[:class:`.Message`]
|
||||||
|
The list of messages that were deleted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if check is MISSING:
|
||||||
|
check = lambda m: True
|
||||||
|
|
||||||
|
iterator = self.history(limit=limit, before=before, after=after, oldest_first=oldest_first, around=around)
|
||||||
|
ret: List[Message] = []
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
|
||||||
|
|
||||||
|
async def _single_delete_strategy(messages: Iterable[Message]):
|
||||||
|
for m in messages:
|
||||||
|
await m.delete()
|
||||||
|
|
||||||
|
strategy = self.delete_messages if bulk else _single_delete_strategy
|
||||||
|
|
||||||
|
async for message in iterator:
|
||||||
|
if count == 100:
|
||||||
|
to_delete = ret[-100:]
|
||||||
|
await strategy(to_delete)
|
||||||
|
count = 0
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
if not check(message):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if message.id < minimum_time:
|
||||||
|
# older than 14 days old
|
||||||
|
if count == 1:
|
||||||
|
await ret[-1].delete()
|
||||||
|
elif count >= 2:
|
||||||
|
to_delete = ret[-count:]
|
||||||
|
await strategy(to_delete)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
strategy = _single_delete_strategy
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
ret.append(message)
|
||||||
|
|
||||||
|
# SOme messages remaining to poll
|
||||||
|
if count >= 2:
|
||||||
|
# more than 2 messages -> bulk delete
|
||||||
|
to_delete = ret[-count:]
|
||||||
|
await strategy(to_delete)
|
||||||
|
elif count == 1:
|
||||||
|
# delete a single message
|
||||||
|
await ret[-1].delete()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str = MISSING,
|
||||||
|
archived: bool = MISSING,
|
||||||
|
locked: bool = MISSING,
|
||||||
|
slowmode_delay: int = MISSING,
|
||||||
|
auto_archive_duration: ThreadArchiveDuration = MISSING,
|
||||||
|
):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Edits the thread.
|
||||||
|
|
||||||
|
Editing the thread requires :attr:`.Permissions.manage_threads`. The thread
|
||||||
|
creator can also edit ``name``, ``archived`` or ``auto_archive_duration``.
|
||||||
|
Note that if the thread is locked then only those with :attr:`.Permissions.manage_threads`
|
||||||
|
can unarchive a thread.
|
||||||
|
|
||||||
|
The thread must be unarchived to be edited.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
name: :class:`str`
|
||||||
|
The new name of the thread.
|
||||||
|
archived: :class:`bool`
|
||||||
|
Whether to archive the thread or not.
|
||||||
|
locked: :class:`bool`
|
||||||
|
Whether to lock the thread or not.
|
||||||
|
auto_archive_duration: :class:`int`
|
||||||
|
The new duration to auto archive threads for inactivity.
|
||||||
|
Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
|
||||||
|
slowmode_delay: :class:`int`
|
||||||
|
Specifies the slowmode rate limit for user in this thread, in seconds.
|
||||||
|
A value of ``0`` disables slowmode. The maximum value possible is ``21600``.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to edit the thread.
|
||||||
|
HTTPException
|
||||||
|
Editing the thread failed.
|
||||||
|
"""
|
||||||
|
payload = {}
|
||||||
|
if name is not MISSING:
|
||||||
|
payload['name'] = str(name)
|
||||||
|
if archived is not MISSING:
|
||||||
|
payload['archived'] = archived
|
||||||
|
if auto_archive_duration is not MISSING:
|
||||||
|
payload['auto_archive_duration'] = auto_archive_duration
|
||||||
|
if locked is not MISSING:
|
||||||
|
payload['locked'] = locked
|
||||||
|
if slowmode_delay is not MISSING:
|
||||||
|
payload['rate_limit_per_user'] = slowmode_delay
|
||||||
|
|
||||||
|
await self._state.http.edit_channel(self.id, **payload)
|
||||||
|
|
||||||
|
async def join(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Joins this thread.
|
||||||
|
|
||||||
|
You must have :attr:`~Permissions.send_messages` and :attr:`~Permissions.use_threads`
|
||||||
|
to join a public thread. If the thread is private then :attr:`~Permissions.send_messages`
|
||||||
|
and either :attr:`~Permissions.use_private_threads` or :attr:`~Permissions.manage_messages`
|
||||||
|
is required to join the thread.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to join the thread.
|
||||||
|
HTTPException
|
||||||
|
Joining the thread failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.join_thread(self.id)
|
||||||
|
|
||||||
|
async def leave(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Leaves this thread.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Leaving the thread failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.leave_thread(self.id)
|
||||||
|
|
||||||
|
async def add_user(self, user: Snowflake):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Adds a user to this thread.
|
||||||
|
|
||||||
|
You must have :attr:`~Permissions.send_messages` and :attr:`~Permissions.use_threads`
|
||||||
|
to add a user to a public thread. If the thread is private then :attr:`~Permissions.send_messages`
|
||||||
|
and either :attr:`~Permissions.use_private_threads` or :attr:`~Permissions.manage_messages`
|
||||||
|
is required to add a user to the thread.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
user: :class:`abc.Snowflake`
|
||||||
|
The user to add to the thread.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to add the user to the thread.
|
||||||
|
HTTPException
|
||||||
|
Adding the user to the thread failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.add_user_to_thread(self.id, user.id)
|
||||||
|
|
||||||
|
async def remove_user(self, user: Snowflake):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Removes a user from this thread.
|
||||||
|
|
||||||
|
You must have :attr:`~Permissions.manage_threads` or be the creator of the thread to remove a user.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
user: :class:`abc.Snowflake`
|
||||||
|
The user to add to the thread.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to remove the user from the thread.
|
||||||
|
HTTPException
|
||||||
|
Removing the user from the thread failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.remove_user_from_thread(self.id, user.id)
|
||||||
|
|
||||||
|
async def delete(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Deletes this thread.
|
||||||
|
|
||||||
|
You must have :attr:`~Permissions.manage_threads` to delete threads.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to delete this thread.
|
||||||
|
HTTPException
|
||||||
|
Deleting the thread failed.
|
||||||
|
"""
|
||||||
|
await self._state.http.delete_channel(self.id)
|
||||||
|
|
||||||
|
def _add_member(self, member: ThreadMember) -> None:
|
||||||
|
self._members[member.id] = member
|
||||||
|
|
||||||
|
def _pop_member(self, member_id: int) -> Optional[ThreadMember]:
|
||||||
|
return self._members.pop(member_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadMember(Hashable):
|
||||||
|
"""Represents a Discord thread member.
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two thread members are equal.
|
||||||
|
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two thread members are not equal.
|
||||||
|
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Returns the thread member's hash.
|
||||||
|
|
||||||
|
.. describe:: str(x)
|
||||||
|
|
||||||
|
Returns the thread member's name.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
id: :class:`int`
|
||||||
|
The thread member's ID.
|
||||||
|
thread_id: :class:`int`
|
||||||
|
The thread's ID.
|
||||||
|
joined_at: :class:`datetime.datetime`
|
||||||
|
The time the member joined the thread in UTC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'id',
|
||||||
|
'thread_id',
|
||||||
|
'joined_at',
|
||||||
|
'flags',
|
||||||
|
'_state',
|
||||||
|
'parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, parent: Thread, data: ThreadMemberPayload):
|
||||||
|
self.parent = parent
|
||||||
|
self._state = parent._state
|
||||||
|
self._from_data(data)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<ThreadMember id={self.id} thread_id={self.thread_id} joined_at={self.joined_at!r}>'
|
||||||
|
|
||||||
|
def _from_data(self, data: ThreadMemberPayload):
|
||||||
|
try:
|
||||||
|
self.id = int(data['user_id'])
|
||||||
|
except KeyError:
|
||||||
|
assert self._state.self_id is not None
|
||||||
|
self.id = self._state.self_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.thread_id = int(data['id'])
|
||||||
|
except KeyError:
|
||||||
|
self.thread_id = self.parent.id
|
||||||
|
|
||||||
|
self.joined_at = parse_time(data['join_timestamp'])
|
||||||
|
self.flags = data['flags']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thread(self) -> Thread:
|
||||||
|
""":class:`Thread`: The thread this member belongs to."""
|
||||||
|
return self.parent
|
||||||
114
discord/types/activity.py
Normal file
114
discord/types/activity.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Literal, Optional, TypedDict
|
||||||
|
from .user import PartialUser
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
StatusType = Literal['idle', 'dnd', 'online', 'offline']
|
||||||
|
|
||||||
|
|
||||||
|
class PartialPresenceUpdate(TypedDict):
|
||||||
|
user: PartialUser
|
||||||
|
guild_id: Snowflake
|
||||||
|
status: StatusType
|
||||||
|
activities: List[Activity]
|
||||||
|
client_status: ClientStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ClientStatus(TypedDict, total=False):
|
||||||
|
desktop: bool
|
||||||
|
mobile: bool
|
||||||
|
web: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityTimestamps(TypedDict, total=False):
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityParty(TypedDict, total=False):
|
||||||
|
id: str
|
||||||
|
size: List[int]
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityAssets(TypedDict, total=False):
|
||||||
|
large_image: str
|
||||||
|
large_text: str
|
||||||
|
small_image: str
|
||||||
|
small_text: str
|
||||||
|
|
||||||
|
|
||||||
|
class ActivitySecrets(TypedDict, total=False):
|
||||||
|
join: str
|
||||||
|
spectate: str
|
||||||
|
match: str
|
||||||
|
|
||||||
|
|
||||||
|
class _ActivityEmojiOptional(TypedDict, total=False):
|
||||||
|
id: Snowflake
|
||||||
|
animated: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityEmoji(_ActivityEmojiOptional):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityButton(TypedDict):
|
||||||
|
label: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class _SendableActivityOptional(TypedDict, total=False):
|
||||||
|
url: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
ActivityType = Literal[0, 1, 2, 4, 5]
|
||||||
|
|
||||||
|
|
||||||
|
class SendableActivity(_SendableActivityOptional):
|
||||||
|
name: str
|
||||||
|
type: ActivityType
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseActivity(SendableActivity):
|
||||||
|
created_at: int
|
||||||
|
|
||||||
|
|
||||||
|
class Activity(_BaseActivity, total=False):
|
||||||
|
state: Optional[str]
|
||||||
|
details: Optional[str]
|
||||||
|
timestamps: ActivityTimestamps
|
||||||
|
assets: ActivityAssets
|
||||||
|
party: ActivityParty
|
||||||
|
application_id: Snowflake
|
||||||
|
flags: int
|
||||||
|
emoji: Optional[ActivityEmoji]
|
||||||
|
secrets: ActivitySecrets
|
||||||
|
session_id: Optional[str]
|
||||||
|
instance: bool
|
||||||
|
buttons: List[ActivityButton]
|
||||||
66
discord/types/appinfo.py
Normal file
66
discord/types/appinfo.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
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 typing import TypedDict, List, Optional
|
||||||
|
|
||||||
|
from .user import User
|
||||||
|
from .team import Team
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
class BaseAppInfo(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
verify_key: str
|
||||||
|
icon: Optional[str]
|
||||||
|
summary: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
class _AppInfoOptional(TypedDict, total=False):
|
||||||
|
team: Team
|
||||||
|
guild_id: Snowflake
|
||||||
|
primary_sku_id: Snowflake
|
||||||
|
slug: str
|
||||||
|
terms_of_service_url: str
|
||||||
|
privacy_policy_url: str
|
||||||
|
hook: bool
|
||||||
|
max_participants: int
|
||||||
|
|
||||||
|
class AppInfo(BaseAppInfo, _AppInfoOptional):
|
||||||
|
rpc_origins: List[str]
|
||||||
|
owner: User
|
||||||
|
bot_public: bool
|
||||||
|
bot_require_code_grant: bool
|
||||||
|
|
||||||
|
class _PartialAppInfoOptional(TypedDict, total=False):
|
||||||
|
rpc_origins: List[str]
|
||||||
|
cover_image: str
|
||||||
|
hook: bool
|
||||||
|
terms_of_service_url: str
|
||||||
|
privacy_policy_url: str
|
||||||
|
max_participants: int
|
||||||
|
|
||||||
|
class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo):
|
||||||
|
pass
|
||||||
240
discord/types/audit_log.py
Normal file
240
discord/types/audit_log.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Literal, Optional, TypedDict, Union
|
||||||
|
from .webhook import Webhook
|
||||||
|
from .guild import MFALevel, VerificationLevel, ExplicitContentFilterLevel, DefaultMessageNotificationLevel
|
||||||
|
from .integration import IntegrationExpireBehavior, PartialIntegration
|
||||||
|
from .user import User
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .role import Role
|
||||||
|
from .channel import ChannelType, VideoQualityMode, PermissionOverwrite
|
||||||
|
|
||||||
|
AuditLogEvent = Literal[
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
|
28,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
32,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
52,
|
||||||
|
60,
|
||||||
|
61,
|
||||||
|
62,
|
||||||
|
72,
|
||||||
|
73,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
82,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_Str(TypedDict):
|
||||||
|
key: Literal[
|
||||||
|
'name', 'description', 'preferred_locale', 'vanity_url_code', 'topic', 'code', 'allow', 'deny', 'permissions'
|
||||||
|
]
|
||||||
|
new_value: str
|
||||||
|
old_value: str
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_AssetHash(TypedDict):
|
||||||
|
key: Literal['icon_hash', 'splash_hash', 'discovery_splash_hash', 'banner_hash', 'avatar_hash']
|
||||||
|
new_value: str
|
||||||
|
old_value: str
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_Snowflake(TypedDict):
|
||||||
|
key: Literal[
|
||||||
|
'id',
|
||||||
|
'owner_id',
|
||||||
|
'afk_channel_id',
|
||||||
|
'rules_channel_id',
|
||||||
|
'public_updates_channel_id',
|
||||||
|
'widget_channel_id',
|
||||||
|
'system_channel_id',
|
||||||
|
'application_id',
|
||||||
|
'channel_id',
|
||||||
|
'inviter_id',
|
||||||
|
]
|
||||||
|
new_value: Snowflake
|
||||||
|
old_value: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_Bool(TypedDict):
|
||||||
|
key: Literal[
|
||||||
|
'widget_enabled',
|
||||||
|
'nsfw',
|
||||||
|
'hoist',
|
||||||
|
'mentionable',
|
||||||
|
'temporary',
|
||||||
|
'deaf',
|
||||||
|
'mute',
|
||||||
|
'nick',
|
||||||
|
'enabled_emoticons',
|
||||||
|
'region',
|
||||||
|
'rtc_region',
|
||||||
|
]
|
||||||
|
new_value: bool
|
||||||
|
old_value: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_Int(TypedDict):
|
||||||
|
key: Literal[
|
||||||
|
'afk_timeout',
|
||||||
|
'prune_delete_days',
|
||||||
|
'position',
|
||||||
|
'bitrate',
|
||||||
|
'rate_limit_per_user',
|
||||||
|
'color',
|
||||||
|
'max_uses',
|
||||||
|
'max_age',
|
||||||
|
'user_limit',
|
||||||
|
]
|
||||||
|
new_value: int
|
||||||
|
old_value: int
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_ListRole(TypedDict):
|
||||||
|
key: Literal['$add', '$remove']
|
||||||
|
new_value: List[Role]
|
||||||
|
old_value: List[Role]
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_MFALevel(TypedDict):
|
||||||
|
key: Literal['mfa_level']
|
||||||
|
new_value: MFALevel
|
||||||
|
old_value: MFALevel
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_VerificationLevel(TypedDict):
|
||||||
|
key: Literal['verification_level']
|
||||||
|
new_value: VerificationLevel
|
||||||
|
old_value: VerificationLevel
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_ExplicitContentFilter(TypedDict):
|
||||||
|
key: Literal['explicit_content_filter']
|
||||||
|
new_value: ExplicitContentFilterLevel
|
||||||
|
old_value: ExplicitContentFilterLevel
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_DefaultMessageNotificationLevel(TypedDict):
|
||||||
|
key: Literal['default_message_notifications']
|
||||||
|
new_value: DefaultMessageNotificationLevel
|
||||||
|
old_value: DefaultMessageNotificationLevel
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_ChannelType(TypedDict):
|
||||||
|
key: Literal['type']
|
||||||
|
new_value: ChannelType
|
||||||
|
old_value: ChannelType
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_IntegrationExpireBehaviour(TypedDict):
|
||||||
|
key: Literal['expire_behavior']
|
||||||
|
new_value: IntegrationExpireBehavior
|
||||||
|
old_value: IntegrationExpireBehavior
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_VideoQualityMode(TypedDict):
|
||||||
|
key: Literal['video_quality_mode']
|
||||||
|
new_value: VideoQualityMode
|
||||||
|
old_value: VideoQualityMode
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogChange_Overwrites(TypedDict):
|
||||||
|
key: Literal['permission_overwrites']
|
||||||
|
new_value: List[PermissionOverwrite]
|
||||||
|
old_value: List[PermissionOverwrite]
|
||||||
|
|
||||||
|
|
||||||
|
AuditLogChange = Union[
|
||||||
|
_AuditLogChange_Str,
|
||||||
|
_AuditLogChange_AssetHash,
|
||||||
|
_AuditLogChange_Snowflake,
|
||||||
|
_AuditLogChange_Int,
|
||||||
|
_AuditLogChange_Bool,
|
||||||
|
_AuditLogChange_ListRole,
|
||||||
|
_AuditLogChange_MFALevel,
|
||||||
|
_AuditLogChange_VerificationLevel,
|
||||||
|
_AuditLogChange_ExplicitContentFilter,
|
||||||
|
_AuditLogChange_DefaultMessageNotificationLevel,
|
||||||
|
_AuditLogChange_ChannelType,
|
||||||
|
_AuditLogChange_IntegrationExpireBehaviour,
|
||||||
|
_AuditLogChange_VideoQualityMode,
|
||||||
|
_AuditLogChange_Overwrites,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AuditEntryInfo(TypedDict):
|
||||||
|
delete_member_days: str
|
||||||
|
members_removed: str
|
||||||
|
channel_id: Snowflake
|
||||||
|
message_id: Snowflake
|
||||||
|
count: str
|
||||||
|
id: Snowflake
|
||||||
|
type: Literal['0', '1']
|
||||||
|
role_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditLogEntryOptional(TypedDict, total=False):
|
||||||
|
changes: List[AuditLogChange]
|
||||||
|
options: AuditEntryInfo
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogEntry(_AuditLogEntryOptional):
|
||||||
|
target_id: Optional[str]
|
||||||
|
user_id: Optional[Snowflake]
|
||||||
|
id: Snowflake
|
||||||
|
action_type: AuditLogEvent
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLog(TypedDict):
|
||||||
|
webhooks: List[Webhook]
|
||||||
|
users: List[User]
|
||||||
|
audit_log_entries: List[AuditLogEntry]
|
||||||
|
integrations: List[PartialIntegration]
|
||||||
@@ -22,58 +22,31 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import List, Literal, Optional, TypedDict, Union
|
||||||
from .user import PartialUser
|
from .user import PartialUser
|
||||||
from .snowflake import Snowflake
|
from .snowflake import Snowflake
|
||||||
from typing import List, Literal, Optional, TypedDict
|
from .threads import ThreadMetadata, ThreadMember
|
||||||
|
|
||||||
|
|
||||||
|
OverwriteType = Literal[0, 1]
|
||||||
|
|
||||||
|
|
||||||
class PermissionOverwrite(TypedDict):
|
class PermissionOverwrite(TypedDict):
|
||||||
id: Snowflake
|
id: Snowflake
|
||||||
type: Literal[0, 1]
|
type: OverwriteType
|
||||||
allow: str
|
allow: str
|
||||||
deny: str
|
deny: str
|
||||||
|
|
||||||
|
|
||||||
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
|
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13]
|
||||||
|
|
||||||
|
|
||||||
class PartialChannel(TypedDict):
|
class _BaseChannel(TypedDict):
|
||||||
id: str
|
id: Snowflake
|
||||||
type: ChannelType
|
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class _TextChannelOptional(PartialChannel, total=False):
|
class _BaseGuildChannel(_BaseChannel):
|
||||||
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
|
guild_id: Snowflake
|
||||||
position: int
|
position: int
|
||||||
permission_overwrites: List[PermissionOverwrite]
|
permission_overwrites: List[PermissionOverwrite]
|
||||||
@@ -81,11 +54,103 @@ class GuildChannel(
|
|||||||
parent_id: Optional[Snowflake]
|
parent_id: Optional[Snowflake]
|
||||||
|
|
||||||
|
|
||||||
class DMChannel(PartialChannel):
|
class PartialChannel(_BaseChannel):
|
||||||
|
type: ChannelType
|
||||||
|
|
||||||
|
|
||||||
|
class _TextChannelOptional(TypedDict, total=False):
|
||||||
|
topic: str
|
||||||
|
last_message_id: Optional[Snowflake]
|
||||||
|
last_pin_timestamp: str
|
||||||
|
rate_limit_per_user: int
|
||||||
|
|
||||||
|
|
||||||
|
class TextChannel(_BaseGuildChannel, _TextChannelOptional):
|
||||||
|
type: Literal[0]
|
||||||
|
|
||||||
|
|
||||||
|
class NewsChannel(_BaseGuildChannel, _TextChannelOptional):
|
||||||
|
type: Literal[5]
|
||||||
|
|
||||||
|
|
||||||
|
VideoQualityMode = Literal[1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class _VoiceChannelOptional(TypedDict, total=False):
|
||||||
|
rtc_region: Optional[str]
|
||||||
|
bitrate: int
|
||||||
|
user_limit: int
|
||||||
|
video_quality_mode: VideoQualityMode
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceChannel(_BaseGuildChannel, _VoiceChannelOptional):
|
||||||
|
type: Literal[2]
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryChannel(_BaseGuildChannel):
|
||||||
|
type: Literal[4]
|
||||||
|
|
||||||
|
|
||||||
|
class StoreChannel(_BaseGuildChannel):
|
||||||
|
type: Literal[6]
|
||||||
|
|
||||||
|
|
||||||
|
class _StageChannelOptional(TypedDict, total=False):
|
||||||
|
rtc_region: Optional[str]
|
||||||
|
bitrate: int
|
||||||
|
user_limit: int
|
||||||
|
topic: str
|
||||||
|
|
||||||
|
|
||||||
|
class StageChannel(_BaseGuildChannel, _StageChannelOptional):
|
||||||
|
type: Literal[13]
|
||||||
|
|
||||||
|
|
||||||
|
class _ThreadChannelOptional(TypedDict, total=False):
|
||||||
|
member: ThreadMember
|
||||||
|
owner_id: Snowflake
|
||||||
|
rate_limit_per_user: int
|
||||||
|
last_message_id: Optional[Snowflake]
|
||||||
|
last_pin_timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadChannel(_BaseChannel, _ThreadChannelOptional):
|
||||||
|
type: Literal[11, 12]
|
||||||
|
guild_id: Snowflake
|
||||||
|
parent_id: Snowflake
|
||||||
|
owner_id: Snowflake
|
||||||
|
nsfw: bool
|
||||||
|
last_message_id: Optional[Snowflake]
|
||||||
|
rate_limit_per_user: int
|
||||||
|
message_count: int
|
||||||
|
member_count: int
|
||||||
|
thread_metadata: ThreadMetadata
|
||||||
|
|
||||||
|
|
||||||
|
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StoreChannel, StageChannel, ThreadChannel]
|
||||||
|
|
||||||
|
|
||||||
|
class DMChannel(_BaseChannel):
|
||||||
|
type: Literal[1]
|
||||||
last_message_id: Optional[Snowflake]
|
last_message_id: Optional[Snowflake]
|
||||||
recipients: List[PartialUser]
|
recipients: List[PartialUser]
|
||||||
|
|
||||||
|
|
||||||
class GroupDMChannel(DMChannel):
|
class GroupDMChannel(_BaseChannel):
|
||||||
|
type: Literal[3]
|
||||||
icon: Optional[str]
|
icon: Optional[str]
|
||||||
owner_id: Snowflake
|
owner_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
Channel = Union[GuildChannel, DMChannel, GroupDMChannel]
|
||||||
|
|
||||||
|
PrivacyLevel = Literal[1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class StageInstance(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
guild_id: Snowflake
|
||||||
|
channel_id: Snowflake
|
||||||
|
topic: str
|
||||||
|
privacy_level: PrivacyLevel
|
||||||
|
discoverable_disabled: bool
|
||||||
|
|||||||
75
discord/types/components.py
Normal file
75
discord/types/components.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Literal, TypedDict, Union
|
||||||
|
from .emoji import PartialEmoji
|
||||||
|
|
||||||
|
ComponentType = Literal[1, 2, 3]
|
||||||
|
ButtonStyle = Literal[1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
|
||||||
|
class ActionRow(TypedDict):
|
||||||
|
type: Literal[1]
|
||||||
|
components: List[Component]
|
||||||
|
|
||||||
|
|
||||||
|
class _ButtonComponentOptional(TypedDict, total=False):
|
||||||
|
custom_id: str
|
||||||
|
url: str
|
||||||
|
disabled: bool
|
||||||
|
emoji: PartialEmoji
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonComponent(_ButtonComponentOptional):
|
||||||
|
type: Literal[2]
|
||||||
|
style: ButtonStyle
|
||||||
|
|
||||||
|
|
||||||
|
class _SelectMenuOptional(TypedDict, total=False):
|
||||||
|
placeholder: str
|
||||||
|
min_values: int
|
||||||
|
max_values: int
|
||||||
|
|
||||||
|
|
||||||
|
class _SelectOptionsOptional(TypedDict, total=False):
|
||||||
|
description: str
|
||||||
|
emoji: PartialEmoji
|
||||||
|
|
||||||
|
|
||||||
|
class SelectOption(_SelectOptionsOptional):
|
||||||
|
label: str
|
||||||
|
value: str
|
||||||
|
default: bool
|
||||||
|
|
||||||
|
|
||||||
|
class SelectMenu(_SelectMenuOptional):
|
||||||
|
type: Literal[3]
|
||||||
|
custom_id: str
|
||||||
|
options: List[SelectOption]
|
||||||
|
|
||||||
|
|
||||||
|
Component = Union[ActionRow, ButtonComponent, SelectMenu]
|
||||||
46
discord/types/emoji.py
Normal file
46
discord/types/emoji.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
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 Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake, SnowflakeList
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class PartialEmoji(TypedDict):
|
||||||
|
id: Optional[Snowflake]
|
||||||
|
name: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Emoji(PartialEmoji, total=False):
|
||||||
|
roles: SnowflakeList
|
||||||
|
user: User
|
||||||
|
require_colons: bool
|
||||||
|
managed: bool
|
||||||
|
animated: bool
|
||||||
|
available: bool
|
||||||
|
|
||||||
|
|
||||||
|
class EditEmoji(TypedDict):
|
||||||
|
name: str
|
||||||
|
roles: Optional[SnowflakeList]
|
||||||
41
discord/types/gateway.py
Normal file
41
discord/types/gateway.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
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 TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStartLimit(TypedDict):
|
||||||
|
total: int
|
||||||
|
remaining: int
|
||||||
|
reset_after: int
|
||||||
|
max_concurrency: int
|
||||||
|
|
||||||
|
|
||||||
|
class Gateway(TypedDict):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayBot(Gateway):
|
||||||
|
shards: int
|
||||||
|
session_start_limit: SessionStartLimit
|
||||||
159
discord/types/guild.py
Normal file
159
discord/types/guild.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
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, Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .channel import GuildChannel
|
||||||
|
from .voice import GuildVoiceState
|
||||||
|
from .welcome_screen import WelcomeScreen
|
||||||
|
from .activity import PartialPresenceUpdate
|
||||||
|
from .role import Role
|
||||||
|
from .member import Member
|
||||||
|
from .emoji import Emoji
|
||||||
|
from .user import User
|
||||||
|
from .threads import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class Ban(TypedDict):
|
||||||
|
reason: Optional[str]
|
||||||
|
user: User
|
||||||
|
|
||||||
|
|
||||||
|
class _UnavailableGuildOptional(TypedDict, total=False):
|
||||||
|
unavailable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class UnavailableGuild(_UnavailableGuildOptional):
|
||||||
|
id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class _GuildOptional(TypedDict, total=False):
|
||||||
|
icon_hash: Optional[str]
|
||||||
|
owner: bool
|
||||||
|
permissions: str
|
||||||
|
widget_enabled: bool
|
||||||
|
widget_channel_id: Optional[Snowflake]
|
||||||
|
joined_at: Optional[str]
|
||||||
|
large: bool
|
||||||
|
member_count: int
|
||||||
|
voice_states: List[GuildVoiceState]
|
||||||
|
members: List[Member]
|
||||||
|
channels: List[GuildChannel]
|
||||||
|
presences: List[PartialPresenceUpdate]
|
||||||
|
threads: List[Thread]
|
||||||
|
max_presences: Optional[int]
|
||||||
|
max_members: int
|
||||||
|
premium_subscription_count: int
|
||||||
|
max_video_channel_users: int
|
||||||
|
|
||||||
|
|
||||||
|
DefaultMessageNotificationLevel = Literal[0, 1]
|
||||||
|
ExplicitContentFilterLevel = Literal[0, 1, 2]
|
||||||
|
MFALevel = Literal[0, 1]
|
||||||
|
VerificationLevel = Literal[0, 1, 2, 3, 4]
|
||||||
|
NSFWLevel = Literal[0, 1, 2, 3]
|
||||||
|
PremiumTier = Literal[0, 1, 2, 3]
|
||||||
|
GuildFeature = Literal[
|
||||||
|
'INVITE_SPLASH',
|
||||||
|
'VIP_REGIONS',
|
||||||
|
'VANITY_URL',
|
||||||
|
'VERIFIED',
|
||||||
|
'PARTNERED',
|
||||||
|
'COMMUNITY',
|
||||||
|
'COMMERCE',
|
||||||
|
'NEWS',
|
||||||
|
'DISCOVERABLE',
|
||||||
|
'FEATURABLE',
|
||||||
|
'ANIMATED_ICON',
|
||||||
|
'BANNER',
|
||||||
|
'WELCOME_SCREEN_ENABLED',
|
||||||
|
'MEMBER_VERIFICATION_GATE_ENABLED',
|
||||||
|
'PREVIEW_ENABLED',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseGuildPreview(UnavailableGuild):
|
||||||
|
name: str
|
||||||
|
icon: Optional[str]
|
||||||
|
splash: Optional[str]
|
||||||
|
discovery_splash: Optional[str]
|
||||||
|
emojis: List[Emoji]
|
||||||
|
features: List[GuildFeature]
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class _GuildPreviewUnique(TypedDict):
|
||||||
|
approximate_member_count: int
|
||||||
|
approximate_presence_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Guild(_BaseGuildPreview, _GuildOptional):
|
||||||
|
owner_id: Snowflake
|
||||||
|
region: str
|
||||||
|
afk_channel_id: Optional[Snowflake]
|
||||||
|
afk_timeout: int
|
||||||
|
verification_level: VerificationLevel
|
||||||
|
default_message_notifications: DefaultMessageNotificationLevel
|
||||||
|
explicit_content_filter: ExplicitContentFilterLevel
|
||||||
|
roles: List[Role]
|
||||||
|
mfa_level: MFALevel
|
||||||
|
nsfw_level: NSFWLevel
|
||||||
|
application_id: Optional[Snowflake]
|
||||||
|
system_channel_id: Optional[Snowflake]
|
||||||
|
system_channel_flags: int
|
||||||
|
rules_channel_id: Optional[Snowflake]
|
||||||
|
vanity_url_code: Optional[str]
|
||||||
|
banner: Optional[str]
|
||||||
|
premium_tier: PremiumTier
|
||||||
|
preferred_locale: str
|
||||||
|
public_updates_channel_id: Optional[Snowflake]
|
||||||
|
|
||||||
|
|
||||||
|
class InviteGuild(Guild, total=False):
|
||||||
|
welcome_screen: WelcomeScreen
|
||||||
|
|
||||||
|
|
||||||
|
class GuildWithCounts(Guild, _GuildPreviewUnique):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class GuildPrune(TypedDict):
|
||||||
|
pruned: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelPositionUpdate(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
position: Optional[int]
|
||||||
|
lock_permissions: Optional[bool]
|
||||||
|
parent_id: Optional[Snowflake]
|
||||||
|
|
||||||
|
class _RolePositionRequired(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
|
||||||
|
class RolePositionUpdate(_RolePositionRequired, total=False):
|
||||||
|
position: Optional[Snowflake]
|
||||||
82
discord/types/integration.py
Normal file
82
discord/types/integration.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Literal, Optional, TypedDict, Union
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class _IntegrationApplicationOptional(TypedDict, total=False):
|
||||||
|
bot: User
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationApplication(_IntegrationApplicationOptional):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
icon: Optional[str]
|
||||||
|
description: str
|
||||||
|
summary: str
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationAccount(TypedDict):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
IntegrationExpireBehavior = Literal[0, 1]
|
||||||
|
|
||||||
|
|
||||||
|
class PartialIntegration(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
type: IntegrationType
|
||||||
|
account: IntegrationAccount
|
||||||
|
|
||||||
|
|
||||||
|
IntegrationType = Literal['twitch', 'youtube', 'discord']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseIntegration(PartialIntegration):
|
||||||
|
enabled: bool
|
||||||
|
syncing: bool
|
||||||
|
synced_at: str
|
||||||
|
user: User
|
||||||
|
expire_behavior: IntegrationExpireBehavior
|
||||||
|
expire_grace_period: int
|
||||||
|
|
||||||
|
|
||||||
|
class StreamIntegration(BaseIntegration):
|
||||||
|
role_id: Snowflake
|
||||||
|
enable_emoticons: bool
|
||||||
|
subscriber_count: int
|
||||||
|
revoked: bool
|
||||||
|
|
||||||
|
|
||||||
|
class BotIntegration(BaseIntegration):
|
||||||
|
application: IntegrationApplication
|
||||||
|
|
||||||
|
|
||||||
|
Integration = Union[BaseIntegration, StreamIntegration, BotIntegration]
|
||||||
189
discord/types/interactions.py
Normal file
189
discord/types/interactions.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Optional, TYPE_CHECKING, Dict, TypedDict, Union, List, Literal
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .components import Component, ComponentType
|
||||||
|
from .embed import Embed
|
||||||
|
from .channel import ChannelType
|
||||||
|
from .member import Member
|
||||||
|
from .role import Role
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .message import AllowedMentions, Message
|
||||||
|
|
||||||
|
|
||||||
|
class _ApplicationCommandOptional(TypedDict, total=False):
|
||||||
|
options: List[ApplicationCommandOption]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommand(_ApplicationCommandOptional):
|
||||||
|
id: Snowflake
|
||||||
|
application_id: Snowflake
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class _ApplicationCommandOptionOptional(TypedDict, total=False):
|
||||||
|
choices: List[ApplicationCommandOptionChoice]
|
||||||
|
options: List[ApplicationCommandOption]
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandOption(_ApplicationCommandOptionOptional):
|
||||||
|
type: ApplicationCommandOptionType
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
required: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandOptionChoice(TypedDict):
|
||||||
|
name: str
|
||||||
|
value: Union[str, int]
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationCommandPermissionType = Literal[1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandPermissions(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
type: ApplicationCommandPermissionType
|
||||||
|
permission: bool
|
||||||
|
|
||||||
|
|
||||||
|
class BaseGuildApplicationCommandPermissions(TypedDict):
|
||||||
|
permissions: List[ApplicationCommandPermissions]
|
||||||
|
|
||||||
|
|
||||||
|
class PartialGuildApplicationCommandPermissions(BaseGuildApplicationCommandPermissions):
|
||||||
|
id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class GuildApplicationCommandPermissions(PartialGuildApplicationCommandPermissions):
|
||||||
|
application_id: Snowflake
|
||||||
|
guild_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
InteractionType = Literal[1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
class _ApplicationCommandInteractionDataOptionOptional(TypedDict, total=False):
|
||||||
|
value: ApplicationCommandOptionType
|
||||||
|
options: List[ApplicationCommandInteractionDataOption]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandInteractionDataOption(
|
||||||
|
_ApplicationCommandInteractionDataOptionOptional
|
||||||
|
):
|
||||||
|
name: str
|
||||||
|
type: ApplicationCommandOptionType
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandResolvedPartialChannel(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
type: ChannelType
|
||||||
|
permissions: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandInteractionDataResolved(TypedDict, total=False):
|
||||||
|
users: Dict[Snowflake, User]
|
||||||
|
members: Dict[Snowflake, Member]
|
||||||
|
roles: Dict[Snowflake, Role]
|
||||||
|
channels: Dict[Snowflake, ApplicationCommandResolvedPartialChannel]
|
||||||
|
|
||||||
|
|
||||||
|
class _ApplicationCommandInteractionDataOptional(TypedDict, total=False):
|
||||||
|
options: List[ApplicationCommandInteractionDataOption]
|
||||||
|
resolved: ApplicationCommandInteractionDataResolved
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOptional):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class _ComponentInteractionDataOptional(TypedDict, total=False):
|
||||||
|
values: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentInteractionData(_ComponentInteractionDataOptional):
|
||||||
|
custom_id: str
|
||||||
|
component_type: ComponentType
|
||||||
|
|
||||||
|
|
||||||
|
class _InteractionOptional(TypedDict, total=False):
|
||||||
|
data: Union[ApplicationCommandInteractionData, ComponentInteractionData]
|
||||||
|
guild_id: Snowflake
|
||||||
|
channel_id: Snowflake
|
||||||
|
member: Member
|
||||||
|
user: User
|
||||||
|
message: Message
|
||||||
|
|
||||||
|
|
||||||
|
class Interaction(_InteractionOptional):
|
||||||
|
id: Snowflake
|
||||||
|
application_id: Snowflake
|
||||||
|
type: InteractionType
|
||||||
|
token: str
|
||||||
|
version: int
|
||||||
|
|
||||||
|
|
||||||
|
class InteractionApplicationCommandCallbackData(TypedDict, total=False):
|
||||||
|
tts: bool
|
||||||
|
content: str
|
||||||
|
embeds: List[Embed]
|
||||||
|
allowed_mentions: AllowedMentions
|
||||||
|
flags: int
|
||||||
|
components: List[Component]
|
||||||
|
|
||||||
|
|
||||||
|
InteractionResponseType = Literal[1, 4, 5, 6, 7]
|
||||||
|
|
||||||
|
|
||||||
|
class _InteractionResponseOptional(TypedDict, total=False):
|
||||||
|
data: InteractionApplicationCommandCallbackData
|
||||||
|
|
||||||
|
|
||||||
|
class InteractionResponse(_InteractionResponseOptional):
|
||||||
|
type: InteractionResponseType
|
||||||
|
|
||||||
|
|
||||||
|
class MessageInteraction(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
type: InteractionType
|
||||||
|
name: str
|
||||||
|
user: User
|
||||||
|
|
||||||
|
|
||||||
|
class EditApplicationCommand(TypedDict):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
options: Optional[List[ApplicationCommandOption]]
|
||||||
|
default_permission: bool
|
||||||
99
discord/types/invite.py
Normal file
99
discord/types/invite.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Literal, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .guild import InviteGuild, _GuildPreviewUnique
|
||||||
|
from .channel import PartialChannel
|
||||||
|
from .user import PartialUser
|
||||||
|
from .appinfo import PartialAppInfo
|
||||||
|
|
||||||
|
InviteTargetType = Literal[1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class _InviteOptional(TypedDict, total=False):
|
||||||
|
guild: InviteGuild
|
||||||
|
inviter: PartialUser
|
||||||
|
target_user: PartialUser
|
||||||
|
target_type: InviteTargetType
|
||||||
|
target_application: PartialAppInfo
|
||||||
|
|
||||||
|
|
||||||
|
class _InviteMetadata(TypedDict, total=False):
|
||||||
|
uses: int
|
||||||
|
max_uses: int
|
||||||
|
max_age: int
|
||||||
|
temporary: bool
|
||||||
|
created_at: str
|
||||||
|
expires_at: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class VanityInvite(_InviteMetadata):
|
||||||
|
code: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class IncompleteInvite(_InviteMetadata):
|
||||||
|
code: str
|
||||||
|
channel: PartialChannel
|
||||||
|
|
||||||
|
|
||||||
|
class Invite(IncompleteInvite, _InviteOptional):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class InviteWithCounts(Invite, _GuildPreviewUnique):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class _GatewayInviteCreateOptional(TypedDict, total=False):
|
||||||
|
guild_id: Snowflake
|
||||||
|
inviter: PartialUser
|
||||||
|
target_type: InviteTargetType
|
||||||
|
target_user: PartialUser
|
||||||
|
target_application: PartialAppInfo
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayInviteCreate(_GatewayInviteCreateOptional):
|
||||||
|
channel_id: Snowflake
|
||||||
|
code: str
|
||||||
|
created_at: str
|
||||||
|
max_age: int
|
||||||
|
max_uses: int
|
||||||
|
temporary: bool
|
||||||
|
uses: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _GatewayInviteDeleteOptional(TypedDict, total=False):
|
||||||
|
guild_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayInviteDelete(_GatewayInviteDeleteOptional):
|
||||||
|
channel_id: Snowflake
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
|
GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete]
|
||||||
46
discord/types/member.py
Normal file
46
discord/types/member.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
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 TypedDict
|
||||||
|
from .snowflake import SnowflakeList
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Nickname(TypedDict):
|
||||||
|
nick: str
|
||||||
|
|
||||||
|
|
||||||
|
class PartialMember(TypedDict):
|
||||||
|
roles: SnowflakeList
|
||||||
|
joined_at: str
|
||||||
|
deaf: str
|
||||||
|
mute: str
|
||||||
|
|
||||||
|
|
||||||
|
class Member(PartialMember, total=False):
|
||||||
|
user: User
|
||||||
|
nick: str
|
||||||
|
premium_since: str
|
||||||
|
pending: bool
|
||||||
|
permissions: str
|
||||||
153
discord/types/message.py
Normal file
153
discord/types/message.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Literal, Optional, TypedDict, Union
|
||||||
|
from .snowflake import Snowflake, SnowflakeList
|
||||||
|
from .member import Member
|
||||||
|
from .user import User
|
||||||
|
from .emoji import PartialEmoji
|
||||||
|
from .embed import Embed
|
||||||
|
from .channel import ChannelType
|
||||||
|
from .components import Component
|
||||||
|
from .interactions import MessageInteraction
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelMention(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
guild_id: Snowflake
|
||||||
|
type: ChannelType
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class Reaction(TypedDict):
|
||||||
|
count: int
|
||||||
|
me: bool
|
||||||
|
emoji: PartialEmoji
|
||||||
|
|
||||||
|
|
||||||
|
class _AttachmentOptional(TypedDict, total=False):
|
||||||
|
height: Optional[int]
|
||||||
|
width: Optional[int]
|
||||||
|
content_type: str
|
||||||
|
spoiler: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Attachment(_AttachmentOptional):
|
||||||
|
id: Snowflake
|
||||||
|
filename: str
|
||||||
|
size: int
|
||||||
|
url: str
|
||||||
|
proxy_url: str
|
||||||
|
|
||||||
|
|
||||||
|
MessageActivityType = Literal[1, 2, 3, 5]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageActivity(TypedDict):
|
||||||
|
type: MessageActivityType
|
||||||
|
party_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class _MessageApplicationOptional(TypedDict, total=False):
|
||||||
|
cover_image: str
|
||||||
|
|
||||||
|
|
||||||
|
class MessageApplication(_MessageApplicationOptional):
|
||||||
|
id: Snowflake
|
||||||
|
description: str
|
||||||
|
icon: Optional[str]
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class MessageReference(TypedDict, total=False):
|
||||||
|
message_id: Snowflake
|
||||||
|
channel_id: Snowflake
|
||||||
|
guild_id: Snowflake
|
||||||
|
fail_if_not_exists: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _StickerOptional(TypedDict, total=False):
|
||||||
|
tags: str
|
||||||
|
|
||||||
|
|
||||||
|
StickerFormatType = Literal[1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
class Sticker(_StickerOptional):
|
||||||
|
id: Snowflake
|
||||||
|
pack_id: Snowflake
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
asset: str
|
||||||
|
format_type: StickerFormatType
|
||||||
|
|
||||||
|
|
||||||
|
class _MessageOptional(TypedDict, total=False):
|
||||||
|
guild_id: Snowflake
|
||||||
|
member: Member
|
||||||
|
mention_channels: List[ChannelMention]
|
||||||
|
reactions: List[Reaction]
|
||||||
|
nonce: Union[int, str]
|
||||||
|
webhook_id: Snowflake
|
||||||
|
activity: MessageActivity
|
||||||
|
application: MessageApplication
|
||||||
|
application_id: Snowflake
|
||||||
|
message_reference: MessageReference
|
||||||
|
flags: int
|
||||||
|
stickers: List[Sticker]
|
||||||
|
referenced_message: Optional[Message]
|
||||||
|
interaction: MessageInteraction
|
||||||
|
components: List[Component]
|
||||||
|
|
||||||
|
|
||||||
|
MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21]
|
||||||
|
|
||||||
|
|
||||||
|
class Message(_MessageOptional):
|
||||||
|
id: Snowflake
|
||||||
|
channel_id: Snowflake
|
||||||
|
author: User
|
||||||
|
content: str
|
||||||
|
timestamp: str
|
||||||
|
edited_timestamp: Optional[str]
|
||||||
|
tts: bool
|
||||||
|
mention_everyone: bool
|
||||||
|
mentions: List[User]
|
||||||
|
mention_roles: SnowflakeList
|
||||||
|
attachments: List[Attachment]
|
||||||
|
embeds: List[Embed]
|
||||||
|
pinned: bool
|
||||||
|
type: MessageType
|
||||||
|
|
||||||
|
|
||||||
|
AllowedMentionType = Literal['roles', 'users', 'everyone']
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedMentions(TypedDict):
|
||||||
|
parse: List[AllowedMentionType]
|
||||||
|
roles: SnowflakeList
|
||||||
|
users: SnowflakeList
|
||||||
|
replied_user: bool
|
||||||
49
discord/types/role.py
Normal file
49
discord/types/role.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
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 typing import TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class _RoleOptional(TypedDict, total=False):
|
||||||
|
tags: RoleTags
|
||||||
|
|
||||||
|
|
||||||
|
class Role(_RoleOptional):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
color: int
|
||||||
|
hoist: bool
|
||||||
|
position: int
|
||||||
|
permissions: str
|
||||||
|
managed: bool
|
||||||
|
mentionable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RoleTags(TypedDict, total=False):
|
||||||
|
bot_id: Snowflake
|
||||||
|
integration_id: Snowflake
|
||||||
|
premium_subscriber: None
|
||||||
@@ -22,7 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
Snowflake = str
|
Snowflake = Union[str, int]
|
||||||
SnowflakeList = List[Snowflake]
|
SnowflakeList = List[Snowflake]
|
||||||
|
|||||||
43
discord/types/team.py
Normal file
43
discord/types/team.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
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 typing import TypedDict, List, Optional
|
||||||
|
|
||||||
|
from .user import PartialUser
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
class TeamMember(TypedDict):
|
||||||
|
user: PartialUser
|
||||||
|
membership_state: int
|
||||||
|
permissions: List[str]
|
||||||
|
team_id: Snowflake
|
||||||
|
|
||||||
|
class Team(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
owner_id: Snowflake
|
||||||
|
members: List[TeamMember]
|
||||||
|
icon: Optional[str]
|
||||||
49
discord/types/template.py
Normal file
49
discord/types/template.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .user import User
|
||||||
|
from .guild import Guild
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTemplate(TypedDict):
|
||||||
|
name: str
|
||||||
|
icon: Optional[bytes]
|
||||||
|
|
||||||
|
|
||||||
|
class Template(TypedDict):
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str]
|
||||||
|
usage_count: int
|
||||||
|
creator_id: Snowflake
|
||||||
|
creator: User
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
source_guild_id: Snowflake
|
||||||
|
serialized_source_guild: Guild
|
||||||
|
is_dirty: Optional[bool]
|
||||||
74
discord/types/threads.py
Normal file
74
discord/types/threads.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Literal, Optional, TypedDict
|
||||||
|
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
ThreadType = Literal[10, 11, 12]
|
||||||
|
ThreadArchiveDuration = Literal[60, 1440, 4320, 10080]
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadMember(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
user_id: Snowflake
|
||||||
|
join_timestamp: str
|
||||||
|
flags: int
|
||||||
|
|
||||||
|
|
||||||
|
class _ThreadMetadataOptional(TypedDict, total=False):
|
||||||
|
archiver_id: Snowflake
|
||||||
|
locked: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadMetadata(_ThreadMetadataOptional):
|
||||||
|
archived: bool
|
||||||
|
auto_archive_duration: ThreadArchiveDuration
|
||||||
|
archive_timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
class _ThreadOptional(TypedDict, total=False):
|
||||||
|
member: ThreadMember
|
||||||
|
last_message_id: Optional[Snowflake]
|
||||||
|
last_pin_timestamp: Optional[Snowflake]
|
||||||
|
|
||||||
|
|
||||||
|
class Thread(_ThreadOptional):
|
||||||
|
id: Snowflake
|
||||||
|
guild_id: Snowflake
|
||||||
|
parent_id: Snowflake
|
||||||
|
owner_id: Snowflake
|
||||||
|
name: str
|
||||||
|
type: ThreadType
|
||||||
|
member_count: int
|
||||||
|
message_count: int
|
||||||
|
rate_limit_per_user: int
|
||||||
|
thread_metadata: ThreadMetadata
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadPaginationPayload(TypedDict):
|
||||||
|
threads: List[Thread]
|
||||||
|
members: List[ThreadMember]
|
||||||
|
has_more: bool
|
||||||
@@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .snowflake import Snowflake
|
from .snowflake import Snowflake
|
||||||
from typing import Optional, TypedDict
|
from typing import Literal, Optional, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class PartialUser(TypedDict):
|
class PartialUser(TypedDict):
|
||||||
@@ -31,3 +31,18 @@ class PartialUser(TypedDict):
|
|||||||
username: str
|
username: str
|
||||||
discriminator: str
|
discriminator: str
|
||||||
avatar: Optional[str]
|
avatar: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
PremiumType = Literal[0, 1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class User(PartialUser, total=False):
|
||||||
|
bot: bool
|
||||||
|
system: bool
|
||||||
|
mfa_enabled: bool
|
||||||
|
local: str
|
||||||
|
verified: bool
|
||||||
|
email: Optional[str]
|
||||||
|
flags: int
|
||||||
|
premium_type: PremiumType
|
||||||
|
public_flags: int
|
||||||
|
|||||||
61
discord/types/voice.py
Normal file
61
discord/types/voice.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
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 Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .member import Member
|
||||||
|
|
||||||
|
|
||||||
|
class _PartialVoiceStateOptional(TypedDict, total=False):
|
||||||
|
member: Member
|
||||||
|
self_stream: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _VoiceState(_PartialVoiceStateOptional):
|
||||||
|
user_id: Snowflake
|
||||||
|
session_id: str
|
||||||
|
deaf: bool
|
||||||
|
mute: bool
|
||||||
|
self_deaf: bool
|
||||||
|
self_mute: bool
|
||||||
|
self_video: bool
|
||||||
|
suppress: bool
|
||||||
|
|
||||||
|
|
||||||
|
class GuildVoiceState(_VoiceState):
|
||||||
|
channel_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceState(_VoiceState, total=False):
|
||||||
|
channel_id: Optional[Snowflake]
|
||||||
|
guild_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceRegion(TypedDict):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
vip: bool
|
||||||
|
optimal: bool
|
||||||
|
deprecated: bool
|
||||||
|
custom: bool
|
||||||
70
discord/types/webhook.py
Normal file
70
discord/types/webhook.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Literal, Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .user import User
|
||||||
|
from .channel import PartialChannel
|
||||||
|
|
||||||
|
|
||||||
|
class SourceGuild(TypedDict):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
class _WebhookOptional(TypedDict, total=False):
|
||||||
|
guild_id: Snowflake
|
||||||
|
user: User
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
WebhookType = Literal[1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
class _FollowerWebhookOptional(TypedDict, total=False):
|
||||||
|
source_channel: PartialChannel
|
||||||
|
source_guild: SourceGuild
|
||||||
|
|
||||||
|
|
||||||
|
class FollowerWebhook(_FollowerWebhookOptional):
|
||||||
|
channel_id: Snowflake
|
||||||
|
webhook_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class PartialWebhook(_WebhookOptional):
|
||||||
|
id: Snowflake
|
||||||
|
type: WebhookType
|
||||||
|
|
||||||
|
|
||||||
|
class _FullWebhook(TypedDict, total=False):
|
||||||
|
name: Optional[str]
|
||||||
|
avatar: Optional[str]
|
||||||
|
channel_id: Snowflake
|
||||||
|
application_id: Optional[Snowflake]
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook(PartialWebhook, _FullWebhook):
|
||||||
|
...
|
||||||
40
discord/types/welcome_screen.py
Normal file
40
discord/types/welcome_screen.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Optional, TypedDict
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeScreen(TypedDict):
|
||||||
|
description: str
|
||||||
|
welcome_channels: List[WelcomeScreenChannel]
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeScreenChannel(TypedDict):
|
||||||
|
channel_id: Snowflake
|
||||||
|
description: str
|
||||||
|
emoji_id: Optional[Snowflake]
|
||||||
|
emoji_name: Optional[str]
|
||||||
63
discord/types/widget.py
Normal file
63
discord/types/widget.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
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, Optional, TypedDict
|
||||||
|
from .activity import Activity
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetChannel(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
position: int
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetMember(User, total=False):
|
||||||
|
nick: str
|
||||||
|
game: Activity
|
||||||
|
status: str
|
||||||
|
avatar_url: str
|
||||||
|
deaf: bool
|
||||||
|
self_deaf: bool
|
||||||
|
mute: bool
|
||||||
|
self_mute: bool
|
||||||
|
suppress: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _WidgetOptional(TypedDict, total=False):
|
||||||
|
channels: List[WidgetChannel]
|
||||||
|
members: List[WidgetMember]
|
||||||
|
presence_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class Widget(_WidgetOptional):
|
||||||
|
id: Snowflake
|
||||||
|
name: str
|
||||||
|
instant_invite: str
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetSettings(TypedDict):
|
||||||
|
enabled: bool
|
||||||
|
channel_id: Optional[Snowflake]
|
||||||
15
discord/ui/__init__.py
Normal file
15
discord/ui/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
discord.ui
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Bot UI Kit helper for the Discord API
|
||||||
|
|
||||||
|
:copyright: (c) 2015-present Rapptz
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .view import *
|
||||||
|
from .item import *
|
||||||
|
from .button import *
|
||||||
|
from .select import *
|
||||||
287
discord/ui/button.py
Normal file
287
discord/ui/button.py
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
from .item import Item, ItemCallbackType
|
||||||
|
from ..enums import ButtonStyle, ComponentType
|
||||||
|
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||||
|
from ..components import Button as ButtonComponent
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Button',
|
||||||
|
'button',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .view import View
|
||||||
|
from ..emoji import Emoji
|
||||||
|
|
||||||
|
B = TypeVar('B', bound='Button')
|
||||||
|
V = TypeVar('V', bound='View', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Button(Item[V]):
|
||||||
|
"""Represents a UI button.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
style: :class:`discord.ButtonStyle`
|
||||||
|
The style of the button.
|
||||||
|
custom_id: Optional[:class:`str`]
|
||||||
|
The ID of the button that gets received during an interaction.
|
||||||
|
If this button is for a URL, it does not have a custom ID.
|
||||||
|
url: Optional[:class:`str`]
|
||||||
|
The URL this button sends you to.
|
||||||
|
disabled: :class:`bool`
|
||||||
|
Whether the button is disabled or not.
|
||||||
|
label: Optional[:class:`str`]
|
||||||
|
The label of the button, if any.
|
||||||
|
emoji: Optional[Union[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]]
|
||||||
|
The emoji of the button, if available.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this button belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
|
'style',
|
||||||
|
'url',
|
||||||
|
'disabled',
|
||||||
|
'label',
|
||||||
|
'emoji',
|
||||||
|
'row',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
style: ButtonStyle,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
custom_id: Optional[str] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||||
|
row: Optional[int] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
if custom_id is not None and url is not None:
|
||||||
|
raise TypeError('cannot mix both url and custom_id with Button')
|
||||||
|
|
||||||
|
self._provided_custom_id = custom_id is not None
|
||||||
|
if url is None and custom_id is None:
|
||||||
|
custom_id = os.urandom(16).hex()
|
||||||
|
|
||||||
|
if url is not None:
|
||||||
|
style = ButtonStyle.link
|
||||||
|
|
||||||
|
if emoji is not None:
|
||||||
|
if isinstance(emoji, str):
|
||||||
|
emoji = PartialEmoji.from_str(emoji)
|
||||||
|
elif isinstance(emoji, _EmojiTag):
|
||||||
|
emoji = emoji._to_partial()
|
||||||
|
else:
|
||||||
|
raise TypeError(f'expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}')
|
||||||
|
|
||||||
|
self._underlying = ButtonComponent._raw_construct(
|
||||||
|
type=ComponentType.button,
|
||||||
|
custom_id=custom_id,
|
||||||
|
url=url,
|
||||||
|
disabled=disabled,
|
||||||
|
label=label,
|
||||||
|
style=style,
|
||||||
|
emoji=emoji,
|
||||||
|
)
|
||||||
|
self.row = row
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self) -> ButtonStyle:
|
||||||
|
""":class:`discord.ButtonStyle`: The style of the button."""
|
||||||
|
return self._underlying.style
|
||||||
|
|
||||||
|
@style.setter
|
||||||
|
def style(self, value: ButtonStyle):
|
||||||
|
self._underlying.style = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_id(self) -> Optional[str]:
|
||||||
|
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
|
||||||
|
|
||||||
|
If this button is for a URL, it does not have a custom ID.
|
||||||
|
"""
|
||||||
|
return self._underlying.custom_id
|
||||||
|
|
||||||
|
@custom_id.setter
|
||||||
|
def custom_id(self, value: Optional[str]):
|
||||||
|
if value is not None and not isinstance(value, str):
|
||||||
|
raise TypeError('custom_id must be None or str')
|
||||||
|
|
||||||
|
self._underlying.custom_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> Optional[str]:
|
||||||
|
"""Optional[:class:`str`]: The URL this button sends you to."""
|
||||||
|
return self._underlying.url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value: Optional[str]):
|
||||||
|
if value is not None and not isinstance(value, str):
|
||||||
|
raise TypeError('url must be None or str')
|
||||||
|
self._underlying.url = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def disabled(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the button is disabled or not."""
|
||||||
|
return self._underlying.disabled
|
||||||
|
|
||||||
|
@disabled.setter
|
||||||
|
def disabled(self, value: bool):
|
||||||
|
self._underlying.disabled = bool(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> Optional[str]:
|
||||||
|
"""Optional[:class:`str`]: The label of the button, if available."""
|
||||||
|
return self._underlying.label
|
||||||
|
|
||||||
|
@label.setter
|
||||||
|
def label(self, value: Optional[str]):
|
||||||
|
self._underlying.label = str(value) if value is not None else value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emoji(self) -> Optional[PartialEmoji]:
|
||||||
|
"""Optional[:class:`PartialEmoji`]: The emoji of the button, if available."""
|
||||||
|
return self._underlying.emoji
|
||||||
|
|
||||||
|
@emoji.setter
|
||||||
|
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
|
||||||
|
if value is not None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._underlying.emoji = PartialEmoji.from_str(value)
|
||||||
|
elif isinstance(value, _EmojiTag):
|
||||||
|
self._underlying.emoji = value._to_partial()
|
||||||
|
else:
|
||||||
|
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead')
|
||||||
|
else:
|
||||||
|
self._underlying.emoji = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_component(cls: Type[B], button: ButtonComponent) -> B:
|
||||||
|
return cls(
|
||||||
|
style=button.style,
|
||||||
|
label=button.label,
|
||||||
|
disabled=button.disabled,
|
||||||
|
custom_id=button.custom_id,
|
||||||
|
url=button.url,
|
||||||
|
emoji=button.emoji,
|
||||||
|
row=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> ComponentType:
|
||||||
|
return self._underlying.type
|
||||||
|
|
||||||
|
def to_component_dict(self):
|
||||||
|
return self._underlying.to_dict()
|
||||||
|
|
||||||
|
def is_dispatchable(self) -> bool:
|
||||||
|
return self.custom_id is not None
|
||||||
|
|
||||||
|
def refresh_component(self, button: ButtonComponent) -> None:
|
||||||
|
self._underlying = button
|
||||||
|
|
||||||
|
|
||||||
|
def button(
|
||||||
|
*,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
custom_id: Optional[str] = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
style: ButtonStyle = ButtonStyle.secondary,
|
||||||
|
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||||
|
row: Optional[int] = None,
|
||||||
|
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||||
|
"""A decorator that attaches a button to a component.
|
||||||
|
|
||||||
|
The function being decorated should have three parameters, ``self`` representing
|
||||||
|
the :class:`discord.ui.View`, the :class:`discord.ui.Button` being pressed and
|
||||||
|
the :class:`discord.Interaction` you receive.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Buttons with a URL cannot be created with this function.
|
||||||
|
Consider creating a :class:`Button` manually instead.
|
||||||
|
This is because buttons with a URL do not have a callback
|
||||||
|
associated with them since Discord does not do any processing
|
||||||
|
with it.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
label: Optional[:class:`str`]
|
||||||
|
The label of the button, if any.
|
||||||
|
custom_id: Optional[:class:`str`]
|
||||||
|
The ID of the button that gets received during an interaction.
|
||||||
|
It is recommended not to set this parameter to prevent conflicts.
|
||||||
|
style: :class:`ButtonStyle`
|
||||||
|
The style of the button. Defaults to :attr:`ButtonStyle.grey`.
|
||||||
|
disabled: :class:`bool`
|
||||||
|
Whether the button is disabled or not. Defaults to ``False``.
|
||||||
|
emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
|
||||||
|
The emoji of the button. This can be in string form or a :class:`PartialEmoji`
|
||||||
|
or a full :class:`Emoji`.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this button belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||||
|
nonlocal custom_id
|
||||||
|
if not inspect.iscoroutinefunction(func):
|
||||||
|
raise TypeError('button function must be a coroutine function')
|
||||||
|
|
||||||
|
custom_id = custom_id or os.urandom(32).hex()
|
||||||
|
func.__discord_ui_model_type__ = Button
|
||||||
|
func.__discord_ui_model_kwargs__ = {
|
||||||
|
'style': style,
|
||||||
|
'custom_id': custom_id,
|
||||||
|
'url': None,
|
||||||
|
'disabled': disabled,
|
||||||
|
'label': label,
|
||||||
|
'emoji': emoji,
|
||||||
|
'row': row,
|
||||||
|
}
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
129
discord/ui/item.py
Normal file
129
discord/ui/item.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar
|
||||||
|
|
||||||
|
from ..interactions import Interaction
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Item',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..enums import ComponentType
|
||||||
|
from .view import View
|
||||||
|
from ..components import Component
|
||||||
|
|
||||||
|
I = TypeVar('I', bound='Item')
|
||||||
|
V = TypeVar('V', bound='View', covariant=True)
|
||||||
|
ItemCallbackType = Callable[[Any, I, Interaction], Coroutine[Any, Any, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Generic[V]):
|
||||||
|
"""Represents the base UI item that all UI components inherit from.
|
||||||
|
|
||||||
|
The current UI items supported are:
|
||||||
|
|
||||||
|
- :class:`discord.ui.Button`
|
||||||
|
"""
|
||||||
|
|
||||||
|
__item_repr_attributes__: Tuple[str, ...] = ('row',)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._view: Optional[V] = None
|
||||||
|
self._row: Optional[int] = None
|
||||||
|
self._rendered_row: Optional[int] = None
|
||||||
|
# This works mostly well but there is a gotcha with
|
||||||
|
# the interaction with from_component, since that technically provides
|
||||||
|
# a custom_id most dispatchable items would get this set to True even though
|
||||||
|
# it might not be provided by the library user. However, this edge case doesn't
|
||||||
|
# actually affect the intended purpose of this check because from_component is
|
||||||
|
# only called upon edit and we're mainly interested during initial creation time.
|
||||||
|
self._provided_custom_id: bool = False
|
||||||
|
|
||||||
|
def to_component_dict(self) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def refresh_component(self, component: Component) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def refresh_state(self, interaction: Interaction) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_component(cls: Type[I], component: Component) -> I:
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> ComponentType:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def is_dispatchable(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_persistent(self) -> bool:
|
||||||
|
return self._provided_custom_id
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
|
||||||
|
return f'<{self.__class__.__name__} {attrs}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row(self) -> Optional[int]:
|
||||||
|
return self._row
|
||||||
|
|
||||||
|
@row.setter
|
||||||
|
def row(self, value: Optional[int]):
|
||||||
|
if value is None:
|
||||||
|
self._row = None
|
||||||
|
elif 5 > value >= 0:
|
||||||
|
self._row = value
|
||||||
|
else:
|
||||||
|
raise ValueError('row cannot be negative or greater than or equal to 5')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
""":class:`int`: The width of the item."""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view(self) -> Optional[V]:
|
||||||
|
"""Optional[:class:`View`]: The underlying view for this item."""
|
||||||
|
return self._view
|
||||||
|
|
||||||
|
async def callback(self, interaction: Interaction):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
The callback associated with this UI item.
|
||||||
|
|
||||||
|
This can be overriden by subclasses.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
interaction: :class:`Interaction`
|
||||||
|
The interaction that triggered this UI item.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
333
discord/ui/select.py
Normal file
333
discord/ui/select.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
"""
|
||||||
|
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 typing import List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type, Callable, Union
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .item import Item, ItemCallbackType
|
||||||
|
from ..enums import ComponentType
|
||||||
|
from ..partial_emoji import PartialEmoji
|
||||||
|
from ..emoji import Emoji
|
||||||
|
from ..interactions import Interaction
|
||||||
|
from ..utils import MISSING
|
||||||
|
from ..components import (
|
||||||
|
SelectOption,
|
||||||
|
SelectMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Select',
|
||||||
|
'select',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .view import View
|
||||||
|
from ..types.components import SelectMenu as SelectMenuPayload
|
||||||
|
from ..types.interactions import (
|
||||||
|
ComponentInteractionData,
|
||||||
|
)
|
||||||
|
|
||||||
|
S = TypeVar('S', bound='Select')
|
||||||
|
V = TypeVar('V', bound='View', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Select(Item[V]):
|
||||||
|
"""Represents a UI select menu.
|
||||||
|
|
||||||
|
This is usually represented as a drop down menu.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
custom_id: :class:`str`
|
||||||
|
The ID of the select menu that gets received during an interaction.
|
||||||
|
If not given then one is generated for you.
|
||||||
|
placeholder: Optional[:class:`str`]
|
||||||
|
The placeholder text that is shown if nothing is selected, if any.
|
||||||
|
min_values: :class:`int`
|
||||||
|
The minimum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
max_values: :class:`int`
|
||||||
|
The maximum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
options: List[:class:`discord.SelectOption`]
|
||||||
|
A list of options that can be selected in this menu.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this select menu belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
|
'placeholder',
|
||||||
|
'min_values',
|
||||||
|
'max_values',
|
||||||
|
'options',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
custom_id: str = MISSING,
|
||||||
|
placeholder: Optional[str] = None,
|
||||||
|
min_values: int = 1,
|
||||||
|
max_values: int = 1,
|
||||||
|
options: List[SelectOption] = MISSING,
|
||||||
|
row: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
self._selected_values: List[str] = []
|
||||||
|
self._provided_custom_id = custom_id is not MISSING
|
||||||
|
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||||
|
options = [] if options is MISSING else options
|
||||||
|
self._underlying = SelectMenu._raw_construct(
|
||||||
|
custom_id=custom_id,
|
||||||
|
type=ComponentType.select,
|
||||||
|
placeholder=placeholder,
|
||||||
|
min_values=min_values,
|
||||||
|
max_values=max_values,
|
||||||
|
options=options,
|
||||||
|
)
|
||||||
|
self.row = row
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_id(self) -> str:
|
||||||
|
""":class:`str`: The ID of the select menu that gets received during an interaction."""
|
||||||
|
return self._underlying.custom_id
|
||||||
|
|
||||||
|
@custom_id.setter
|
||||||
|
def custom_id(self, value: str):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError('custom_id must be None or str')
|
||||||
|
|
||||||
|
self._underlying.custom_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def placeholder(self) -> Optional[str]:
|
||||||
|
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any."""
|
||||||
|
return self._underlying.placeholder
|
||||||
|
|
||||||
|
@placeholder.setter
|
||||||
|
def placeholder(self, value: Optional[str]):
|
||||||
|
if value is not None and not isinstance(value, str):
|
||||||
|
raise TypeError('placeholder must be None or str')
|
||||||
|
|
||||||
|
self._underlying.placeholder = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_values(self) -> int:
|
||||||
|
""":class:`int`: The minimum number of items that must be chosen for this select menu."""
|
||||||
|
return self._underlying.min_values
|
||||||
|
|
||||||
|
@min_values.setter
|
||||||
|
def min_values(self, value: int):
|
||||||
|
self._underlying.min_values = int(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_values(self) -> int:
|
||||||
|
""":class:`int`: The maximum number of items that must be chosen for this select menu."""
|
||||||
|
return self._underlying.max_values
|
||||||
|
|
||||||
|
@max_values.setter
|
||||||
|
def max_values(self, value: int):
|
||||||
|
self._underlying.max_values = int(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> List[SelectOption]:
|
||||||
|
"""List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu."""
|
||||||
|
return self._underlying.options
|
||||||
|
|
||||||
|
@options.setter
|
||||||
|
def options(self, value: List[SelectOption]):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise TypeError('options must be a list of SelectOption')
|
||||||
|
if not all(isinstance(obj, SelectOption) for obj in value):
|
||||||
|
raise TypeError('all list items must subclass SelectOption')
|
||||||
|
|
||||||
|
self._underlying.options = value
|
||||||
|
|
||||||
|
def add_option(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
value: str = MISSING,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||||
|
default: bool = False,
|
||||||
|
):
|
||||||
|
"""Adds an option to the select menu.
|
||||||
|
|
||||||
|
To append a pre-existing :class:`discord.SelectOption` use the
|
||||||
|
:meth:`append_option` method instead.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
label: :class:`str`
|
||||||
|
The label of the option. This is displayed to users.
|
||||||
|
Can only be up to 25 characters.
|
||||||
|
value: :class:`str`
|
||||||
|
The value of the option. This is not displayed to users.
|
||||||
|
If not given, defaults to the label. Can only be up to 100 characters.
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
An additional description of the option, if any.
|
||||||
|
Can only be up to 50 characters.
|
||||||
|
emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
|
||||||
|
The emoji of the option, if available. This can either be a string representing
|
||||||
|
the custom or unicode emoji or an instance of :class:`PartialEmoji` or :class:`Emoji`.
|
||||||
|
default: :class:`bool`
|
||||||
|
Whether this option is selected by default.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError
|
||||||
|
The number of options exceeds 25.
|
||||||
|
"""
|
||||||
|
|
||||||
|
option = SelectOption(
|
||||||
|
label=label,
|
||||||
|
value=value,
|
||||||
|
description=description,
|
||||||
|
emoji=emoji,
|
||||||
|
default=default,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
self.append_option(option)
|
||||||
|
|
||||||
|
def append_option(self, option: SelectOption):
|
||||||
|
"""Appends an option to the select menu.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
option: :class:`discord.SelectOption`
|
||||||
|
The option to append to the select menu.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError
|
||||||
|
The number of options exceeds 25.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self._underlying.options) > 25:
|
||||||
|
raise ValueError('maximum number of options already provided')
|
||||||
|
|
||||||
|
self._underlying.options.append(option)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> List[str]:
|
||||||
|
"""List[:class:`str`]: A list of values that have been selected by the user."""
|
||||||
|
return self._selected_values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
return 5
|
||||||
|
|
||||||
|
def to_component_dict(self) -> SelectMenuPayload:
|
||||||
|
return self._underlying.to_dict()
|
||||||
|
|
||||||
|
def refresh_component(self, component: SelectMenu) -> None:
|
||||||
|
self._underlying = component
|
||||||
|
|
||||||
|
def refresh_state(self, interaction: Interaction) -> None:
|
||||||
|
data: ComponentInteractionData = interaction.data # type: ignore
|
||||||
|
self._selected_values = data.get('values', [])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_component(cls: Type[S], component: SelectMenu) -> S:
|
||||||
|
return cls(
|
||||||
|
custom_id=component.custom_id,
|
||||||
|
placeholder=component.placeholder,
|
||||||
|
min_values=component.min_values,
|
||||||
|
max_values=component.max_values,
|
||||||
|
options=component.options,
|
||||||
|
row=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> ComponentType:
|
||||||
|
return self._underlying.type
|
||||||
|
|
||||||
|
def is_dispatchable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def select(
|
||||||
|
*,
|
||||||
|
placeholder: Optional[str] = None,
|
||||||
|
custom_id: str = MISSING,
|
||||||
|
min_values: int = 1,
|
||||||
|
max_values: int = 1,
|
||||||
|
options: List[SelectOption] = MISSING,
|
||||||
|
row: Optional[int] = None,
|
||||||
|
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||||
|
"""A decorator that attaches a select menu to a component.
|
||||||
|
|
||||||
|
The function being decorated should have three parameters, ``self`` representing
|
||||||
|
the :class:`discord.ui.View`, the :class:`discord.ui.Select` being pressed and
|
||||||
|
the :class:`discord.Interaction` you receive.
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
placeholder: Optional[:class:`str`]
|
||||||
|
The placeholder text that is shown if nothing is selected, if any.
|
||||||
|
custom_id: :class:`str`
|
||||||
|
The ID of the select menu that gets received during an interaction.
|
||||||
|
It is recommended not to set this parameter to prevent conflicts.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this select menu belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||||
|
min_values: :class:`int`
|
||||||
|
The minimum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
max_values: :class:`int`
|
||||||
|
The maximum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 and 25.
|
||||||
|
options: List[:class:`discord.SelectOption`]
|
||||||
|
A list of options that can be selected in this menu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||||
|
if not inspect.iscoroutinefunction(func):
|
||||||
|
raise TypeError('button function must be a coroutine function')
|
||||||
|
|
||||||
|
func.__discord_ui_model_type__ = Select
|
||||||
|
func.__discord_ui_model_kwargs__ = {
|
||||||
|
'placeholder': placeholder,
|
||||||
|
'custom_id': custom_id,
|
||||||
|
'row': row,
|
||||||
|
'min_values': min_values,
|
||||||
|
'max_values': max_values,
|
||||||
|
'options': options,
|
||||||
|
}
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
463
discord/ui/view.py
Normal file
463
discord/ui/view.py
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
"""
|
||||||
|
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 typing import Any, Callable, ClassVar, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple
|
||||||
|
from functools import partial
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from .item import Item, ItemCallbackType
|
||||||
|
from ..enums import ComponentType
|
||||||
|
from ..components import (
|
||||||
|
Component,
|
||||||
|
ActionRow as ActionRowComponent,
|
||||||
|
_component_factory,
|
||||||
|
Button as ButtonComponent,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'View',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..interactions import Interaction
|
||||||
|
from ..types.components import Component as ComponentPayload
|
||||||
|
from ..state import ConnectionState
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
||||||
|
for item in components:
|
||||||
|
if isinstance(item, ActionRowComponent):
|
||||||
|
yield from item.children
|
||||||
|
else:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
|
||||||
|
def _component_to_item(component: Component) -> Item:
|
||||||
|
if isinstance(component, ButtonComponent):
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
return Button.from_component(component)
|
||||||
|
return Item.from_component(component)
|
||||||
|
|
||||||
|
|
||||||
|
class _ViewWeights:
|
||||||
|
__slots__ = (
|
||||||
|
'weights',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, children: List[Item]):
|
||||||
|
self.weights: List[int] = [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
key = lambda i: sys.maxsize if i.row is None else i.row
|
||||||
|
children = sorted(children, key=key)
|
||||||
|
for row, group in groupby(children, key=key):
|
||||||
|
for item in group:
|
||||||
|
self.add_item(item)
|
||||||
|
|
||||||
|
def find_open_space(self, item: Item) -> int:
|
||||||
|
for index, weight in enumerate(self.weights):
|
||||||
|
if weight + item.width <= 5:
|
||||||
|
return index
|
||||||
|
|
||||||
|
raise ValueError('could not find open space for item')
|
||||||
|
|
||||||
|
def add_item(self, item: Item) -> None:
|
||||||
|
if item.row is not None:
|
||||||
|
total = self.weights[item.row] + item.width
|
||||||
|
if total > 5:
|
||||||
|
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)')
|
||||||
|
self.weights[item.row] = total
|
||||||
|
item._rendered_row = item.row
|
||||||
|
else:
|
||||||
|
index = self.find_open_space(item)
|
||||||
|
self.weights[index] += item.width
|
||||||
|
item._rendered_row = index
|
||||||
|
|
||||||
|
def remove_item(self, item: Item) -> None:
|
||||||
|
if item._rendered_row is not None:
|
||||||
|
self.weights[item._rendered_row] -= item.width
|
||||||
|
item._rendered_row = None
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.weights = [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
class View:
|
||||||
|
"""Represents a UI view.
|
||||||
|
|
||||||
|
This object must be inherited to create a UI within Discord.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
timeout: Optional[:class:`float`]
|
||||||
|
Timeout from last interaction with the UI before no longer accepting input.
|
||||||
|
If ``None`` then there is no timeout.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
------------
|
||||||
|
timeout: Optional[:class:`float`]
|
||||||
|
Timeout from last interaction with the UI before no longer accepting input.
|
||||||
|
If ``None`` then there is no timeout.
|
||||||
|
children: List[:class:`Item`]
|
||||||
|
The list of children attached to this view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__discord_ui_view__: ClassVar[bool] = True
|
||||||
|
__view_children_items__: ClassVar[List[ItemCallbackType]] = []
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
children: List[ItemCallbackType] = []
|
||||||
|
for base in reversed(cls.__mro__):
|
||||||
|
for member in base.__dict__.values():
|
||||||
|
if hasattr(member, '__discord_ui_model_type__'):
|
||||||
|
children.append(member)
|
||||||
|
|
||||||
|
if len(children) > 25:
|
||||||
|
raise TypeError('View cannot have more than 25 children')
|
||||||
|
|
||||||
|
cls.__view_children_items__ = children
|
||||||
|
|
||||||
|
def __init__(self, timeout: Optional[float] = 180.0):
|
||||||
|
self.timeout = timeout
|
||||||
|
self.children: List[Item] = []
|
||||||
|
for func in self.__view_children_items__:
|
||||||
|
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||||
|
item.callback = partial(func, self, item)
|
||||||
|
item._view = self
|
||||||
|
setattr(self, func.__name__, item)
|
||||||
|
self.children.append(item)
|
||||||
|
|
||||||
|
self.__weights = _ViewWeights(self.children)
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
self.id = os.urandom(16).hex()
|
||||||
|
self._cancel_callback: Optional[Callable[[View], None]] = None
|
||||||
|
self._timeout_handler: Optional[asyncio.TimerHandle] = None
|
||||||
|
self._stopped = loop.create_future()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>'
|
||||||
|
|
||||||
|
def to_components(self) -> List[Dict[str, Any]]:
|
||||||
|
def key(item: Item) -> int:
|
||||||
|
return item._rendered_row or 0
|
||||||
|
|
||||||
|
children = sorted(self.children, key=key)
|
||||||
|
components: List[Dict[str, Any]] = []
|
||||||
|
for _, group in groupby(children, key=key):
|
||||||
|
children = [item.to_component_dict() for item in group]
|
||||||
|
if not children:
|
||||||
|
continue
|
||||||
|
|
||||||
|
components.append(
|
||||||
|
{
|
||||||
|
'type': 1,
|
||||||
|
'components': children,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _expires_at(self) -> Optional[float]:
|
||||||
|
if self.timeout:
|
||||||
|
return time.monotonic() + self.timeout
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_item(self, item: Item) -> None:
|
||||||
|
"""Adds an item to the view.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
item: :class:`Item`
|
||||||
|
The item to add to the view.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
--------
|
||||||
|
TypeError
|
||||||
|
An :class:`Item` was not passed.
|
||||||
|
ValueError
|
||||||
|
Maximum number of children has been exceeded (25)
|
||||||
|
or the row the item is trying to be added to is full.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self.children) > 25:
|
||||||
|
raise ValueError('maximum number of children exceeded')
|
||||||
|
|
||||||
|
if not isinstance(item, Item):
|
||||||
|
raise TypeError(f'expected Item not {item.__class__!r}')
|
||||||
|
|
||||||
|
self.__weights.add_item(item)
|
||||||
|
|
||||||
|
item._view = self
|
||||||
|
self.children.append(item)
|
||||||
|
|
||||||
|
def remove_item(self, item: Item) -> None:
|
||||||
|
"""Removes an item from the view.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
item: :class:`Item`
|
||||||
|
The item to remove from the view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.children.remove(item)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.__weights.remove_item(item)
|
||||||
|
|
||||||
|
def clear_items(self) -> None:
|
||||||
|
"""Removes all items from the view."""
|
||||||
|
self.children.clear()
|
||||||
|
self.__weights.clear()
|
||||||
|
|
||||||
|
async def interaction_check(self, interaction: Interaction) -> bool:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A callback that is called when an interaction happens within the view
|
||||||
|
that checks whether the view should process item callbacks for the interaction.
|
||||||
|
|
||||||
|
This is useful to override if, for example, you want to ensure that the
|
||||||
|
interaction author is a given user.
|
||||||
|
|
||||||
|
The default implementation of this returns ``True``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If an exception occurs within the body then the interaction
|
||||||
|
check then :meth:`on_error` is called and it is considered
|
||||||
|
a failure.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
interaction: :class:`~discord.Interaction`
|
||||||
|
The interaction that occurred.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
---------
|
||||||
|
:class:`bool`
|
||||||
|
Whether the view children's callbacks should be called.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def on_timeout(self) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A callback that is called when a view's timeout elapses without being explicitly stopped.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_error(self, error: Exception, item: Item, interaction: Interaction) -> None:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A callback that is called when an item's callback or :meth:`interaction_check`
|
||||||
|
fails with an error.
|
||||||
|
|
||||||
|
The default implementation prints the traceback to stderr.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
error: :class:`Exception`
|
||||||
|
The exception that was raised.
|
||||||
|
item: :class:`Item`
|
||||||
|
The item that failed the dispatch.
|
||||||
|
interaction: :class:`~discord.Interaction`
|
||||||
|
The interaction that led to the failure.
|
||||||
|
"""
|
||||||
|
print(f'Ignoring exception in view {self} for item {item}:', file=sys.stderr)
|
||||||
|
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
|
||||||
|
|
||||||
|
async def _scheduled_task(self, state: Any, item: Item, interaction: Interaction):
|
||||||
|
try:
|
||||||
|
allow = await self.interaction_check(interaction)
|
||||||
|
if not allow:
|
||||||
|
return
|
||||||
|
|
||||||
|
await item.callback(interaction)
|
||||||
|
if not interaction.response._responded:
|
||||||
|
await interaction.response.defer()
|
||||||
|
except Exception as e:
|
||||||
|
return await self.on_error(e, item, interaction)
|
||||||
|
|
||||||
|
def _start_listening(self, store: ViewStore) -> None:
|
||||||
|
self._cancel_callback = partial(store.remove_view)
|
||||||
|
if self.timeout:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
self._timeout_handler = loop.call_later(self.timeout, self.dispatch_timeout)
|
||||||
|
|
||||||
|
def dispatch_timeout(self):
|
||||||
|
if self._stopped.done():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stopped.set_result(True)
|
||||||
|
asyncio.create_task(self.on_timeout(), name=f'discord-ui-view-timeout-{self.id}')
|
||||||
|
|
||||||
|
def dispatch(self, state: Any, item: Item, interaction: Interaction):
|
||||||
|
asyncio.create_task(self._scheduled_task(state, item, interaction), name=f'discord-ui-view-dispatch-{self.id}')
|
||||||
|
|
||||||
|
def refresh(self, components: List[Component]):
|
||||||
|
# This is pretty hacky at the moment
|
||||||
|
# fmt: off
|
||||||
|
old_state: Dict[Tuple[int, str], Item] = {
|
||||||
|
(item.type.value, item.custom_id): item # type: ignore
|
||||||
|
for item in self.children
|
||||||
|
if item.is_dispatchable()
|
||||||
|
}
|
||||||
|
# fmt: on
|
||||||
|
children: List[Item] = []
|
||||||
|
for component in _walk_all_components(components):
|
||||||
|
try:
|
||||||
|
older = old_state[(component.type.value, component.custom_id)] # type: ignore
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
children.append(_component_to_item(component))
|
||||||
|
else:
|
||||||
|
older.refresh_component(component)
|
||||||
|
children.append(older)
|
||||||
|
|
||||||
|
self.children = children
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stops listening to interaction events from this view.
|
||||||
|
|
||||||
|
This operation cannot be undone.
|
||||||
|
"""
|
||||||
|
if not self._stopped.done():
|
||||||
|
self._stopped.set_result(False)
|
||||||
|
|
||||||
|
if self._timeout_handler:
|
||||||
|
self._timeout_handler.cancel()
|
||||||
|
|
||||||
|
if self._cancel_callback:
|
||||||
|
self._cancel_callback(self)
|
||||||
|
self._cancel_callback = None
|
||||||
|
|
||||||
|
def is_finished(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the view has finished interacting."""
|
||||||
|
return self._stopped.done()
|
||||||
|
|
||||||
|
def is_dispatching(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the view has been added for dispatching purposes."""
|
||||||
|
return self._cancel_callback is not None
|
||||||
|
|
||||||
|
def is_persistent(self) -> bool:
|
||||||
|
""":class:`bool`: Whether the view is set up as persistent.
|
||||||
|
|
||||||
|
A persistent view has all their components with a set ``custom_id`` and
|
||||||
|
a :attr:`timeout` set to ``None``.
|
||||||
|
"""
|
||||||
|
return self.timeout is None and all(item.is_persistent() for item in self.children)
|
||||||
|
|
||||||
|
async def wait(self) -> bool:
|
||||||
|
"""Waits until the view has finished interacting.
|
||||||
|
|
||||||
|
A view is considered finished when :meth:`stop` is called
|
||||||
|
or it times out.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`bool`
|
||||||
|
If ``True``, then the view timed out. If ``False`` then
|
||||||
|
the view finished normally.
|
||||||
|
"""
|
||||||
|
return await self._stopped
|
||||||
|
|
||||||
|
|
||||||
|
class ViewStore:
|
||||||
|
def __init__(self, state: ConnectionState):
|
||||||
|
# (component_type, custom_id): (View, Item, Expiry)
|
||||||
|
self._views: Dict[Tuple[int, str], Tuple[View, Item, Optional[float]]] = {}
|
||||||
|
# message_id: View
|
||||||
|
self._synced_message_views: Dict[int, View] = {}
|
||||||
|
self._state: ConnectionState = state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persistent_views(self) -> Sequence[View]:
|
||||||
|
views = {
|
||||||
|
view.id: view
|
||||||
|
for (_, (view, _, _)) in self._views.items()
|
||||||
|
if view.is_persistent()
|
||||||
|
}
|
||||||
|
return list(views.values())
|
||||||
|
|
||||||
|
def __verify_integrity(self):
|
||||||
|
to_remove: List[Tuple[int, str]] = []
|
||||||
|
now = time.monotonic()
|
||||||
|
for (k, (_, _, expiry)) in self._views.items():
|
||||||
|
if expiry is not None and now >= expiry:
|
||||||
|
to_remove.append(k)
|
||||||
|
|
||||||
|
for k in to_remove:
|
||||||
|
del self._views[k]
|
||||||
|
|
||||||
|
def add_view(self, view: View, message_id: Optional[int] = None):
|
||||||
|
self.__verify_integrity()
|
||||||
|
|
||||||
|
expiry = view._expires_at
|
||||||
|
view._start_listening(self)
|
||||||
|
for item in view.children:
|
||||||
|
if item.is_dispatchable():
|
||||||
|
self._views[(item.type.value, item.custom_id)] = (view, item, expiry) # type: ignore
|
||||||
|
|
||||||
|
if message_id is not None:
|
||||||
|
self._synced_message_views[message_id] = view
|
||||||
|
|
||||||
|
def remove_view(self, view: View):
|
||||||
|
for item in view.children:
|
||||||
|
if item.is_dispatchable():
|
||||||
|
self._views.pop((item.type.value, item.custom_id), None) # type: ignore
|
||||||
|
|
||||||
|
for key, value in self._synced_message_views.items():
|
||||||
|
if value.id == view.id:
|
||||||
|
del self._synced_message_views[key]
|
||||||
|
break
|
||||||
|
|
||||||
|
def dispatch(self, component_type: int, custom_id: str, interaction: Interaction):
|
||||||
|
self.__verify_integrity()
|
||||||
|
key = (component_type, custom_id)
|
||||||
|
value = self._views.get(key)
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
view, item, _ = value
|
||||||
|
self._views[key] = (view, item, view._expires_at)
|
||||||
|
item.refresh_state(interaction)
|
||||||
|
view.dispatch(self._state, item, interaction)
|
||||||
|
|
||||||
|
def is_message_tracked(self, message_id: int):
|
||||||
|
return message_id in self._synced_message_views
|
||||||
|
|
||||||
|
def remove_message_tracking(self, message_id: int) -> Optional[View]:
|
||||||
|
return self._synced_message_views.pop(message_id, None)
|
||||||
|
|
||||||
|
def update_from_message(self, message_id: int, components: List[ComponentPayload]):
|
||||||
|
# pre-req: is_message_tracked == true
|
||||||
|
view = self._synced_message_views[message_id]
|
||||||
|
view.refresh([_component_factory(d) for d in components])
|
||||||
128
discord/user.py
128
discord/user.py
@@ -22,10 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
import discord.abc
|
import discord.abc
|
||||||
from .flags import PublicUserFlags
|
from .flags import PublicUserFlags
|
||||||
from .utils import snowflake_time, _bytes_to_base64_data
|
from .utils import snowflake_time, _bytes_to_base64_data
|
||||||
from .enums import DefaultAvatar, try_enum
|
from .enums import DefaultAvatar
|
||||||
from .colour import Colour
|
from .colour import Colour
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
|
|
||||||
@@ -36,15 +37,29 @@ __all__ = (
|
|||||||
|
|
||||||
_BaseUser = discord.abc.User
|
_BaseUser = discord.abc.User
|
||||||
|
|
||||||
|
|
||||||
class BaseUser(_BaseUser):
|
class BaseUser(_BaseUser):
|
||||||
__slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', 'system', '_public_flags', '_state')
|
__slots__ = ('name', 'id', 'discriminator', '_avatar', 'bot', 'system', '_public_flags', '_state')
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
name: str
|
||||||
|
id: int
|
||||||
|
discriminator: str
|
||||||
|
bot: bool
|
||||||
|
system: bool
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
def __init__(self, *, state, data):
|
||||||
self._state = state
|
self._state = state
|
||||||
self._update(data)
|
self._update(data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<BaseUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}"
|
||||||
|
f" bot={self.bot} system={self.system}>"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{0.name}#{0.discriminator}'.format(self)
|
return f'{self.name}#{self.discriminator}'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, _BaseUser) and other.id == self.id
|
return isinstance(other, _BaseUser) and other.id == self.id
|
||||||
@@ -59,19 +74,19 @@ class BaseUser(_BaseUser):
|
|||||||
self.name = data['username']
|
self.name = data['username']
|
||||||
self.id = int(data['id'])
|
self.id = int(data['id'])
|
||||||
self.discriminator = data['discriminator']
|
self.discriminator = data['discriminator']
|
||||||
self.avatar = data['avatar']
|
self._avatar = data['avatar']
|
||||||
self._public_flags = data.get('public_flags', 0)
|
self._public_flags = data.get('public_flags', 0)
|
||||||
self.bot = data.get('bot', False)
|
self.bot = data.get('bot', False)
|
||||||
self.system = data.get('system', False)
|
self.system = data.get('system', False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _copy(cls, user):
|
def _copy(cls, user):
|
||||||
self = cls.__new__(cls) # bypass __init__
|
self = cls.__new__(cls) # bypass __init__
|
||||||
|
|
||||||
self.name = user.name
|
self.name = user.name
|
||||||
self.id = user.id
|
self.id = user.id
|
||||||
self.discriminator = user.discriminator
|
self.discriminator = user.discriminator
|
||||||
self.avatar = user.avatar
|
self._avatar = user._avatar
|
||||||
self.bot = user.bot
|
self.bot = user.bot
|
||||||
self._state = user._state
|
self._state = user._state
|
||||||
self._public_flags = user._public_flags
|
self._public_flags = user._public_flags
|
||||||
@@ -82,7 +97,7 @@ class BaseUser(_BaseUser):
|
|||||||
return {
|
return {
|
||||||
'username': self.name,
|
'username': self.name,
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'avatar': self.avatar,
|
'avatar': self._avatar,
|
||||||
'discriminator': self.discriminator,
|
'discriminator': self.discriminator,
|
||||||
'bot': self.bot,
|
'bot': self.bot,
|
||||||
}
|
}
|
||||||
@@ -93,66 +108,20 @@ class BaseUser(_BaseUser):
|
|||||||
return PublicUserFlags._from_value(self._public_flags)
|
return PublicUserFlags._from_value(self._public_flags)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def avatar_url(self):
|
def avatar(self):
|
||||||
""":class:`Asset`: Returns an :class:`Asset` for the avatar the user has.
|
""":class:`Asset`: Returns an :class:`Asset` for the avatar the user has.
|
||||||
|
|
||||||
If the user does not have a traditional avatar, an asset for
|
If the user does not have a traditional avatar, an asset for
|
||||||
the default avatar is returned instead.
|
the default avatar is returned instead.
|
||||||
|
|
||||||
This is equivalent to calling :meth:`avatar_url_as` with
|
|
||||||
the default parameters (i.e. webp/gif detection and a size of 1024).
|
|
||||||
"""
|
"""
|
||||||
return self.avatar_url_as(format=None, size=1024)
|
if self._avatar is None:
|
||||||
|
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
|
||||||
def is_avatar_animated(self):
|
else:
|
||||||
""":class:`bool`: Indicates if the user has an animated avatar."""
|
return Asset._from_avatar(self._state, self.id, self._avatar)
|
||||||
return bool(self.avatar and self.avatar.startswith('a_'))
|
|
||||||
|
|
||||||
def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
|
|
||||||
"""Returns an :class:`Asset` for the avatar the user has.
|
|
||||||
|
|
||||||
If the user does not have a traditional avatar, an asset for
|
|
||||||
the default avatar is returned instead.
|
|
||||||
|
|
||||||
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 avatar to.
|
|
||||||
If the format is ``None``, then it is automatically
|
|
||||||
detected into either 'gif' or static_format depending on the
|
|
||||||
avatar being animated or not.
|
|
||||||
static_format: Optional[:class:`str`]
|
|
||||||
Format to attempt to convert only non-animated avatars to.
|
|
||||||
Defaults to 'webp'
|
|
||||||
size: :class:`int`
|
|
||||||
The size of the image to display.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InvalidArgument
|
|
||||||
Bad image format passed to ``format`` or ``static_format``, or
|
|
||||||
invalid ``size``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Asset`
|
|
||||||
The resulting CDN asset.
|
|
||||||
"""
|
|
||||||
return Asset._from_avatar(self._state, self, format=format, static_format=static_format, size=size)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_avatar(self):
|
def default_avatar(self):
|
||||||
""":class:`DefaultAvatar`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
|
""":class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
|
||||||
return try_enum(DefaultAvatar, int(self.discriminator) % len(DefaultAvatar))
|
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
|
||||||
|
|
||||||
@property
|
|
||||||
def default_avatar_url(self):
|
|
||||||
""":class:`Asset`: Returns a URL for a user's default avatar."""
|
|
||||||
return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colour(self):
|
def colour(self):
|
||||||
@@ -177,22 +146,6 @@ class BaseUser(_BaseUser):
|
|||||||
""":class:`str`: Returns a string that allows you to mention the given user."""
|
""":class:`str`: Returns a string that allows you to mention the given user."""
|
||||||
return f'<@{self.id}>'
|
return f'<@{self.id}>'
|
||||||
|
|
||||||
def permissions_in(self, channel):
|
|
||||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
|
||||||
|
|
||||||
Basically equivalent to:
|
|
||||||
|
|
||||||
.. code-block:: python3
|
|
||||||
|
|
||||||
channel.permissions_for(self)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
channel: :class:`abc.GuildChannel`
|
|
||||||
The channel to check your permissions for.
|
|
||||||
"""
|
|
||||||
return channel.permissions_for(self)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self):
|
||||||
""":class:`datetime.datetime`: Returns the user's creation time in UTC.
|
""":class:`datetime.datetime`: Returns the user's creation time in UTC.
|
||||||
@@ -230,6 +183,7 @@ class BaseUser(_BaseUser):
|
|||||||
|
|
||||||
return any(user.id == self.id for user in message.mentions)
|
return any(user.id == self.id for user in message.mentions)
|
||||||
|
|
||||||
|
|
||||||
class ClientUser(BaseUser):
|
class ClientUser(BaseUser):
|
||||||
"""Represents your Discord user.
|
"""Represents your Discord user.
|
||||||
|
|
||||||
@@ -259,8 +213,6 @@ class ClientUser(BaseUser):
|
|||||||
The user's unique ID.
|
The user's unique ID.
|
||||||
discriminator: :class:`str`
|
discriminator: :class:`str`
|
||||||
The user's discriminator. This is given when the username has conflicts.
|
The user's discriminator. This is given when the username has conflicts.
|
||||||
avatar: Optional[:class:`str`]
|
|
||||||
The avatar hash the user has. Could be ``None``.
|
|
||||||
bot: :class:`bool`
|
bot: :class:`bool`
|
||||||
Specifies if the user is a bot account.
|
Specifies if the user is a bot account.
|
||||||
system: :class:`bool`
|
system: :class:`bool`
|
||||||
@@ -269,21 +221,23 @@ class ClientUser(BaseUser):
|
|||||||
.. versionadded:: 1.3
|
.. versionadded:: 1.3
|
||||||
|
|
||||||
verified: :class:`bool`
|
verified: :class:`bool`
|
||||||
Specifies if the user is a verified account.
|
Specifies if the user's email is verified.
|
||||||
locale: Optional[:class:`str`]
|
locale: Optional[:class:`str`]
|
||||||
The IETF language tag used to identify the language the user is using.
|
The IETF language tag used to identify the language the user is using.
|
||||||
mfa_enabled: :class:`bool`
|
mfa_enabled: :class:`bool`
|
||||||
Specifies if the user has MFA turned on and working.
|
Specifies if the user has MFA turned on and working.
|
||||||
"""
|
"""
|
||||||
__slots__ = BaseUser.__slots__ + \
|
|
||||||
('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
|
__slots__ = BaseUser.__slots__ + ('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
def __init__(self, *, state, data):
|
||||||
super().__init__(state=state, data=data)
|
super().__init__(state=state, data=data)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
|
return (
|
||||||
' bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>'.format(self)
|
f'<ClientUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}'
|
||||||
|
f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled}>'
|
||||||
|
)
|
||||||
|
|
||||||
def _update(self, data):
|
def _update(self, data):
|
||||||
super()._update(data)
|
super()._update(data)
|
||||||
@@ -293,8 +247,7 @@ class ClientUser(BaseUser):
|
|||||||
self._flags = data.get('flags', 0)
|
self._flags = data.get('flags', 0)
|
||||||
self.mfa_enabled = data.get('mfa_enabled', False)
|
self.mfa_enabled = data.get('mfa_enabled', False)
|
||||||
|
|
||||||
|
async def edit(self, *, username: str = None, avatar: Optional[bytes] = None) -> None:
|
||||||
async def edit(self, *, username=None, avatar=None):
|
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Edits the current profile of the client.
|
Edits the current profile of the client.
|
||||||
@@ -330,6 +283,7 @@ class ClientUser(BaseUser):
|
|||||||
data = await self._state.http.edit_profile(username=username, avatar=avatar)
|
data = await self._state.http.edit_profile(username=username, avatar=avatar)
|
||||||
self._update(data)
|
self._update(data)
|
||||||
|
|
||||||
|
|
||||||
class User(BaseUser, discord.abc.Messageable):
|
class User(BaseUser, discord.abc.Messageable):
|
||||||
"""Represents a Discord user.
|
"""Represents a Discord user.
|
||||||
|
|
||||||
@@ -359,8 +313,6 @@ class User(BaseUser, discord.abc.Messageable):
|
|||||||
The user's unique ID.
|
The user's unique ID.
|
||||||
discriminator: :class:`str`
|
discriminator: :class:`str`
|
||||||
The user's discriminator. This is given when the username has conflicts.
|
The user's discriminator. This is given when the username has conflicts.
|
||||||
avatar: Optional[:class:`str`]
|
|
||||||
The avatar hash the user has. Could be None.
|
|
||||||
bot: :class:`bool`
|
bot: :class:`bool`
|
||||||
Specifies if the user is a bot account.
|
Specifies if the user is a bot account.
|
||||||
system: :class:`bool`
|
system: :class:`bool`
|
||||||
@@ -370,7 +322,7 @@ class User(BaseUser, discord.abc.Messageable):
|
|||||||
__slots__ = BaseUser.__slots__ + ('__weakref__',)
|
__slots__ = BaseUser.__slots__ + ('__weakref__',)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.format(self)
|
return f'<User id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot}>'
|
||||||
|
|
||||||
async def _get_channel(self):
|
async def _get_channel(self):
|
||||||
ch = await self.create_dm()
|
ch = await self.create_dm()
|
||||||
|
|||||||
446
discord/utils.py
446
discord/utils.py
@@ -21,11 +21,33 @@ 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
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import array
|
import array
|
||||||
import asyncio
|
import asyncio
|
||||||
import collections.abc
|
import collections.abc
|
||||||
from typing import Optional, overload
|
from typing import (
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
ForwardRef,
|
||||||
|
Generic,
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
overload,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from bisect import bisect_left
|
from bisect import bisect_left
|
||||||
@@ -35,12 +57,14 @@ from inspect import isawaitable as _isawaitable, signature as _signature
|
|||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from .errors import InvalidArgument
|
from .errors import InvalidArgument
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'oauth_uri',
|
'oauth_url',
|
||||||
'snowflake_time',
|
'snowflake_time',
|
||||||
'time_snowflake',
|
'time_snowflake',
|
||||||
'find',
|
'find',
|
||||||
@@ -50,10 +74,27 @@ __all__ = (
|
|||||||
'remove_markdown',
|
'remove_markdown',
|
||||||
'escape_markdown',
|
'escape_markdown',
|
||||||
'escape_mentions',
|
'escape_mentions',
|
||||||
|
'as_chunks',
|
||||||
)
|
)
|
||||||
|
|
||||||
DISCORD_EPOCH = 1420070400000
|
DISCORD_EPOCH = 1420070400000
|
||||||
|
|
||||||
class cached_property:
|
|
||||||
|
class _MissingSentinel:
|
||||||
|
def __eq__(self, other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '...'
|
||||||
|
|
||||||
|
|
||||||
|
MISSING: Any = _MissingSentinel()
|
||||||
|
|
||||||
|
|
||||||
|
class _cached_property:
|
||||||
def __init__(self, function):
|
def __init__(self, function):
|
||||||
self.function = function
|
self.function = function
|
||||||
self.__doc__ = getattr(function, '__doc__')
|
self.__doc__ = getattr(function, '__doc__')
|
||||||
@@ -67,13 +108,42 @@ class cached_property:
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class CachedSlotProperty:
|
|
||||||
def __init__(self, name, function):
|
if TYPE_CHECKING:
|
||||||
|
from functools import cached_property as cached_property
|
||||||
|
from .permissions import Permissions
|
||||||
|
from .abc import Snowflake
|
||||||
|
from .invite import Invite
|
||||||
|
from .template import Template
|
||||||
|
|
||||||
|
class _RequestLike(Protocol):
|
||||||
|
headers: Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
cached_property = _cached_property
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
T_co = TypeVar('T_co', covariant=True)
|
||||||
|
_Iter = Union[Iterator[T], AsyncIterator[T]]
|
||||||
|
|
||||||
|
|
||||||
|
class CachedSlotProperty(Generic[T, T_co]):
|
||||||
|
def __init__(self, name: str, function: Callable[[T], T_co]) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.function = function
|
self.function = function
|
||||||
self.__doc__ = getattr(function, '__doc__')
|
self.__doc__ = getattr(function, '__doc__')
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
@overload
|
||||||
|
def __get__(self, instance: None, owner: Type[T]) -> CachedSlotProperty[T, T_co]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(self, instance: T, owner: Type[T]) -> T_co:
|
||||||
|
...
|
||||||
|
|
||||||
|
def __get__(self, instance: Optional[T], owner: Type[T]) -> Any:
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -84,74 +154,109 @@ class CachedSlotProperty:
|
|||||||
setattr(instance, self.name, value)
|
setattr(instance, self.name, value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def cached_slot_property(name):
|
|
||||||
def decorator(func):
|
class classproperty(Generic[T_co]):
|
||||||
|
def __init__(self, fget: Callable[[Any], T_co]) -> None:
|
||||||
|
self.fget = fget
|
||||||
|
|
||||||
|
def __get__(self, instance: Optional[Any], owner: Type[Any]) -> T_co:
|
||||||
|
return self.fget(owner)
|
||||||
|
|
||||||
|
def __set__(self, instance, value) -> None:
|
||||||
|
raise AttributeError('cannot set attribute')
|
||||||
|
|
||||||
|
|
||||||
|
def cached_slot_property(name: str) -> Callable[[Callable[[T], T_co]], CachedSlotProperty[T, T_co]]:
|
||||||
|
def decorator(func: Callable[[T], T_co]) -> CachedSlotProperty[T, T_co]:
|
||||||
return CachedSlotProperty(name, func)
|
return CachedSlotProperty(name, func)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
class SequenceProxy(collections.abc.Sequence):
|
|
||||||
|
class SequenceProxy(Generic[T_co], collections.abc.Sequence):
|
||||||
"""Read-only proxy of a Sequence."""
|
"""Read-only proxy of a Sequence."""
|
||||||
def __init__(self, proxied):
|
|
||||||
|
def __init__(self, proxied: Sequence[T_co]):
|
||||||
self.__proxied = proxied
|
self.__proxied = proxied
|
||||||
|
|
||||||
def __getitem__(self, idx):
|
def __getitem__(self, idx: int) -> T_co:
|
||||||
return self.__proxied[idx]
|
return self.__proxied[idx]
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self.__proxied)
|
return len(self.__proxied)
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item: Any) -> bool:
|
||||||
return item in self.__proxied
|
return item in self.__proxied
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> Iterator[T_co]:
|
||||||
return iter(self.__proxied)
|
return iter(self.__proxied)
|
||||||
|
|
||||||
def __reversed__(self):
|
def __reversed__(self) -> Iterator[T_co]:
|
||||||
return reversed(self.__proxied)
|
return reversed(self.__proxied)
|
||||||
|
|
||||||
def index(self, value, *args, **kwargs):
|
def index(self, value: Any, *args, **kwargs) -> int:
|
||||||
return self.__proxied.index(value, *args, **kwargs)
|
return self.__proxied.index(value, *args, **kwargs)
|
||||||
|
|
||||||
def count(self, value):
|
def count(self, value: Any) -> int:
|
||||||
return self.__proxied.count(value)
|
return self.__proxied.count(value)
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_time(timestamp: None) -> None:
|
def parse_time(timestamp: None) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_time(timestamp: str) -> datetime.datetime:
|
def parse_time(timestamp: str) -> datetime.datetime:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
|
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
|
||||||
if timestamp:
|
if timestamp:
|
||||||
return datetime.datetime.fromisoformat(timestamp)
|
return datetime.datetime.fromisoformat(timestamp)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def copy_doc(original):
|
|
||||||
def decorator(overriden):
|
def copy_doc(original: Callable[..., Any]) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||||
|
def decorator(overriden: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
overriden.__doc__ = original.__doc__
|
overriden.__doc__ = original.__doc__
|
||||||
overriden.__signature__ = _signature(original)
|
overriden.__signature__ = _signature(original) # type: ignore
|
||||||
return overriden
|
return overriden
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def deprecated(instead=None):
|
|
||||||
def actual_decorator(func):
|
def deprecated(instead: Optional[str] = None) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
||||||
|
def actual_decorator(func: Callable[..., T]) -> Callable[..., T]:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs) -> T:
|
||||||
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
||||||
if instead:
|
if instead:
|
||||||
fmt = "{0.__name__} is deprecated, use {1} instead."
|
fmt = "{0.__name__} is deprecated, use {1} instead."
|
||||||
else:
|
else:
|
||||||
fmt = '{0.__name__} is deprecated.'
|
fmt = '{0.__name__} is deprecated.'
|
||||||
|
|
||||||
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
|
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
|
||||||
warnings.simplefilter('default', DeprecationWarning) # reset filter
|
warnings.simplefilter('default', DeprecationWarning) # reset filter
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
return actual_decorator
|
return actual_decorator
|
||||||
|
|
||||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes=None):
|
|
||||||
|
def oauth_url(
|
||||||
|
client_id: str,
|
||||||
|
permissions: Optional[Permissions] = None,
|
||||||
|
guild: Optional[Snowflake] = None,
|
||||||
|
redirect_uri: Optional[str] = None,
|
||||||
|
scopes: Optional[Iterable[str]] = None,
|
||||||
|
):
|
||||||
"""A helper function that returns the OAuth2 URL for inviting the bot
|
"""A helper function that returns the OAuth2 URL for inviting the bot
|
||||||
into guilds.
|
into guilds.
|
||||||
|
|
||||||
@@ -162,7 +267,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
|
|||||||
permissions: :class:`~discord.Permissions`
|
permissions: :class:`~discord.Permissions`
|
||||||
The permissions you're requesting. If not given then you won't be requesting any
|
The permissions you're requesting. If not given then you won't be requesting any
|
||||||
permissions.
|
permissions.
|
||||||
guild: :class:`~discord.Guild`
|
guild: :class:`~discord.abc.Snowflake`
|
||||||
The guild to pre-select in the authorization screen, if available.
|
The guild to pre-select in the authorization screen, if available.
|
||||||
redirect_uri: :class:`str`
|
redirect_uri: :class:`str`
|
||||||
An optional valid redirect URI.
|
An optional valid redirect URI.
|
||||||
@@ -184,6 +289,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
|
|||||||
url = url + "&guild_id=" + str(guild.id)
|
url = url + "&guild_id=" + str(guild.id)
|
||||||
if redirect_uri is not None:
|
if redirect_uri is not None:
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
url = url + "&response_type=code&" + urlencode({'redirect_uri': redirect_uri})
|
url = url + "&response_type=code&" + urlencode({'redirect_uri': redirect_uri})
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@@ -203,6 +309,7 @@ def snowflake_time(id: int) -> datetime.datetime:
|
|||||||
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
|
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
|
||||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
|
def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
|
||||||
"""Returns a numeric snowflake pretending to be created at the given date.
|
"""Returns a numeric snowflake pretending to be created at the given date.
|
||||||
|
|
||||||
@@ -226,9 +333,10 @@ def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
|
|||||||
The snowflake representing the time given.
|
The snowflake representing the time given.
|
||||||
"""
|
"""
|
||||||
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
|
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
|
||||||
return (discord_millis << 22) + (2**22-1 if high else 0)
|
return (discord_millis << 22) + (2 ** 22 - 1 if high else 0)
|
||||||
|
|
||||||
def find(predicate, seq):
|
|
||||||
|
def find(predicate: Callable[[T], Any], seq: Iterable[T]) -> Optional[T]:
|
||||||
"""A helper to return the first element found in the sequence
|
"""A helper to return the first element found in the sequence
|
||||||
that meets the predicate. For example: ::
|
that meets the predicate. For example: ::
|
||||||
|
|
||||||
@@ -244,7 +352,7 @@ def find(predicate, seq):
|
|||||||
-----------
|
-----------
|
||||||
predicate
|
predicate
|
||||||
A function that returns a boolean-like result.
|
A function that returns a boolean-like result.
|
||||||
seq: iterable
|
seq: :class:`collections.abc.Iterable`
|
||||||
The iterable to search through.
|
The iterable to search through.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -253,7 +361,8 @@ def find(predicate, seq):
|
|||||||
return element
|
return element
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(iterable, **attrs):
|
|
||||||
|
def get(iterable: Iterable[T], **attrs: Any) -> Optional[T]:
|
||||||
r"""A helper that returns the first element in the iterable that meets
|
r"""A helper that returns the first element in the iterable that meets
|
||||||
all the traits passed in ``attrs``. This is an alternative for
|
all the traits passed in ``attrs``. This is an alternative for
|
||||||
:func:`~discord.utils.find`.
|
:func:`~discord.utils.find`.
|
||||||
@@ -310,22 +419,19 @@ def get(iterable, **attrs):
|
|||||||
return elem
|
return elem
|
||||||
return None
|
return None
|
||||||
|
|
||||||
converted = [
|
converted = [(attrget(attr.replace('__', '.')), value) for attr, value in attrs.items()]
|
||||||
(attrget(attr.replace('__', '.')), value)
|
|
||||||
for attr, value in attrs.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
for elem in iterable:
|
for elem in iterable:
|
||||||
if _all(pred(elem) == value for pred, value in converted):
|
if _all(pred(elem) == value for pred, value in converted):
|
||||||
return elem
|
return elem
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _unique(iterable):
|
|
||||||
seen = set()
|
|
||||||
adder = seen.add
|
|
||||||
return [x for x in iterable if not (x in seen or adder(x))]
|
|
||||||
|
|
||||||
def _get_as_snowflake(data, key):
|
def _unique(iterable: Iterable[T]) -> List[T]:
|
||||||
|
return [x for x in dict.fromkeys(iterable)]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_as_snowflake(data: Any, key: str) -> Optional[int]:
|
||||||
try:
|
try:
|
||||||
value = data[key]
|
value = data[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -333,7 +439,8 @@ def _get_as_snowflake(data, key):
|
|||||||
else:
|
else:
|
||||||
return value and int(value)
|
return value and int(value)
|
||||||
|
|
||||||
def _get_mime_type_for_image(data):
|
|
||||||
|
def _get_mime_type_for_image(data: bytes):
|
||||||
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
|
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
|
||||||
return 'image/png'
|
return 'image/png'
|
||||||
elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'):
|
elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'):
|
||||||
@@ -345,17 +452,20 @@ def _get_mime_type_for_image(data):
|
|||||||
else:
|
else:
|
||||||
raise InvalidArgument('Unsupported image type given')
|
raise InvalidArgument('Unsupported image type given')
|
||||||
|
|
||||||
def _bytes_to_base64_data(data):
|
|
||||||
|
def _bytes_to_base64_data(data: bytes) -> str:
|
||||||
fmt = 'data:{mime};base64,{data}'
|
fmt = 'data:{mime};base64,{data}'
|
||||||
mime = _get_mime_type_for_image(data)
|
mime = _get_mime_type_for_image(data)
|
||||||
b64 = b64encode(data).decode('ascii')
|
b64 = b64encode(data).decode('ascii')
|
||||||
return fmt.format(mime=mime, data=b64)
|
return fmt.format(mime=mime, data=b64)
|
||||||
|
|
||||||
def to_json(obj):
|
|
||||||
|
def to_json(obj: Any) -> str:
|
||||||
return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
|
return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
|
||||||
|
|
||||||
def _parse_ratelimit_header(request, *, use_clock=False):
|
|
||||||
reset_after = request.headers.get('X-Ratelimit-Reset-After')
|
def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
|
||||||
|
reset_after: Optional[str] = request.headers.get('X-Ratelimit-Reset-After')
|
||||||
if use_clock or not reset_after:
|
if use_clock or not reset_after:
|
||||||
utc = datetime.timezone.utc
|
utc = datetime.timezone.utc
|
||||||
now = datetime.datetime.now(utc)
|
now = datetime.datetime.now(utc)
|
||||||
@@ -364,6 +474,7 @@ def _parse_ratelimit_header(request, *, use_clock=False):
|
|||||||
else:
|
else:
|
||||||
return float(reset_after)
|
return float(reset_after)
|
||||||
|
|
||||||
|
|
||||||
async def maybe_coroutine(f, *args, **kwargs):
|
async def maybe_coroutine(f, *args, **kwargs):
|
||||||
value = f(*args, **kwargs)
|
value = f(*args, **kwargs)
|
||||||
if _isawaitable(value):
|
if _isawaitable(value):
|
||||||
@@ -371,6 +482,7 @@ async def maybe_coroutine(f, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
async def async_all(gen, *, check=_isawaitable):
|
async def async_all(gen, *, check=_isawaitable):
|
||||||
for elem in gen:
|
for elem in gen:
|
||||||
if check(elem):
|
if check(elem):
|
||||||
@@ -379,10 +491,9 @@ async def async_all(gen, *, check=_isawaitable):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def sane_wait_for(futures, *, timeout):
|
async def sane_wait_for(futures, *, timeout):
|
||||||
ensured = [
|
ensured = [asyncio.ensure_future(fut) for fut in futures]
|
||||||
asyncio.ensure_future(fut) for fut in futures
|
|
||||||
]
|
|
||||||
done, pending = await asyncio.wait(ensured, timeout=timeout, return_when=asyncio.ALL_COMPLETED)
|
done, pending = await asyncio.wait(ensured, timeout=timeout, return_when=asyncio.ALL_COMPLETED)
|
||||||
|
|
||||||
if len(pending) != 0:
|
if len(pending) != 0:
|
||||||
@@ -390,7 +501,23 @@ async def sane_wait_for(futures, *, timeout):
|
|||||||
|
|
||||||
return done
|
return done
|
||||||
|
|
||||||
async def sleep_until(when, result=None):
|
|
||||||
|
def get_slots(cls: Type[Any]) -> Iterator[str]:
|
||||||
|
for mro in reversed(cls.__mro__):
|
||||||
|
try:
|
||||||
|
yield from mro.__slots__
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def compute_timedelta(dt: datetime.datetime):
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.astimezone()
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
return max((dt - now).total_seconds(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def sleep_until(when: datetime.datetime, result: Optional[T] = None) -> Optional[T]:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Sleep until a specified time.
|
Sleep until a specified time.
|
||||||
@@ -407,16 +534,14 @@ async def sleep_until(when, result=None):
|
|||||||
result: Any
|
result: Any
|
||||||
If provided is returned to the caller when the coroutine completes.
|
If provided is returned to the caller when the coroutine completes.
|
||||||
"""
|
"""
|
||||||
if when.tzinfo is None:
|
delta = compute_timedelta(when)
|
||||||
when = when.astimezone()
|
return await asyncio.sleep(delta, result)
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
delta = (when - now).total_seconds()
|
|
||||||
return await asyncio.sleep(max(delta, 0), result)
|
|
||||||
|
|
||||||
def utcnow() -> datetime.datetime:
|
def utcnow() -> datetime.datetime:
|
||||||
"""A helper function to return an aware UTC datetime representing the current time.
|
"""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
|
This should be preferred to :meth:`datetime.datetime.utcnow` since it is an aware
|
||||||
datetime, compared to the naive datetime in the standard library.
|
datetime, compared to the naive datetime in the standard library.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
@@ -428,9 +553,11 @@ def utcnow() -> datetime.datetime:
|
|||||||
"""
|
"""
|
||||||
return datetime.datetime.now(datetime.timezone.utc)
|
return datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
def valid_icon_size(size):
|
|
||||||
|
def valid_icon_size(size: int) -> bool:
|
||||||
"""Icons must be power of 2 within [16, 4096]."""
|
"""Icons must be power of 2 within [16, 4096]."""
|
||||||
return not size & (size - 1) and size in range(16, 4097)
|
return not size & (size - 1) and 4096 >= size >= 16
|
||||||
|
|
||||||
|
|
||||||
class SnowflakeList(array.array):
|
class SnowflakeList(array.array):
|
||||||
"""Internal data storage class to efficiently store a list of snowflakes.
|
"""Internal data storage class to efficiently store a list of snowflakes.
|
||||||
@@ -446,24 +573,31 @@ class SnowflakeList(array.array):
|
|||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __new__(cls, data, *, is_sorted=False):
|
if TYPE_CHECKING:
|
||||||
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data))
|
|
||||||
|
|
||||||
def add(self, element):
|
def __init__(self, data: Iterable[int], *, is_sorted: bool = False):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __new__(cls, data: Iterable[int], *, is_sorted: bool = False):
|
||||||
|
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data)) # type: ignore
|
||||||
|
|
||||||
|
def add(self, element: int) -> None:
|
||||||
i = bisect_left(self, element)
|
i = bisect_left(self, element)
|
||||||
self.insert(i, element)
|
self.insert(i, element)
|
||||||
|
|
||||||
def get(self, element):
|
def get(self, element: int) -> Optional[int]:
|
||||||
i = bisect_left(self, element)
|
i = bisect_left(self, element)
|
||||||
return self[i] if i != len(self) and self[i] == element else None
|
return self[i] if i != len(self) and self[i] == element else None
|
||||||
|
|
||||||
def has(self, element):
|
def has(self, element: int) -> bool:
|
||||||
i = bisect_left(self, element)
|
i = bisect_left(self, element)
|
||||||
return i != len(self) and self[i] == element
|
return i != len(self) and self[i] == element
|
||||||
|
|
||||||
|
|
||||||
_IS_ASCII = re.compile(r'^[\x00-\x7f]+$')
|
_IS_ASCII = re.compile(r'^[\x00-\x7f]+$')
|
||||||
|
|
||||||
def _string_width(string, *, _IS_ASCII=_IS_ASCII):
|
|
||||||
|
def _string_width(string: str, *, _IS_ASCII=_IS_ASCII) -> int:
|
||||||
"""Returns string's width."""
|
"""Returns string's width."""
|
||||||
match = _IS_ASCII.match(string)
|
match = _IS_ASCII.match(string)
|
||||||
if match:
|
if match:
|
||||||
@@ -473,7 +607,8 @@ def _string_width(string, *, _IS_ASCII=_IS_ASCII):
|
|||||||
func = unicodedata.east_asian_width
|
func = unicodedata.east_asian_width
|
||||||
return sum(2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1 for char in string)
|
return sum(2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1 for char in string)
|
||||||
|
|
||||||
def resolve_invite(invite):
|
|
||||||
|
def resolve_invite(invite: Union[Invite, str]) -> str:
|
||||||
"""
|
"""
|
||||||
Resolves an invite from a :class:`~discord.Invite`, URL or code.
|
Resolves an invite from a :class:`~discord.Invite`, URL or code.
|
||||||
|
|
||||||
@@ -488,6 +623,7 @@ def resolve_invite(invite):
|
|||||||
The invite code.
|
The invite code.
|
||||||
"""
|
"""
|
||||||
from .invite import Invite # circular import
|
from .invite import Invite # circular import
|
||||||
|
|
||||||
if isinstance(invite, Invite):
|
if isinstance(invite, Invite):
|
||||||
return invite.code
|
return invite.code
|
||||||
else:
|
else:
|
||||||
@@ -497,7 +633,8 @@ def resolve_invite(invite):
|
|||||||
return m.group(1)
|
return m.group(1)
|
||||||
return invite
|
return invite
|
||||||
|
|
||||||
def resolve_template(code):
|
|
||||||
|
def resolve_template(code: Union[Template, str]) -> str:
|
||||||
"""
|
"""
|
||||||
Resolves a template code from a :class:`~discord.Template`, URL or code.
|
Resolves a template code from a :class:`~discord.Template`, URL or code.
|
||||||
|
|
||||||
@@ -513,7 +650,8 @@ def resolve_template(code):
|
|||||||
:class:`str`
|
:class:`str`
|
||||||
The template code.
|
The template code.
|
||||||
"""
|
"""
|
||||||
from .template import Template # circular import
|
from .template import Template # circular import
|
||||||
|
|
||||||
if isinstance(code, Template):
|
if isinstance(code, Template):
|
||||||
return code.code
|
return code.code
|
||||||
else:
|
else:
|
||||||
@@ -523,8 +661,8 @@ def resolve_template(code):
|
|||||||
return m.group(1)
|
return m.group(1)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c)
|
|
||||||
for c in ('*', '`', '_', '~', '|'))
|
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c) for c in ('*', '`', '_', '~', '|'))
|
||||||
|
|
||||||
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
|
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
|
||||||
|
|
||||||
@@ -534,7 +672,8 @@ _URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\
|
|||||||
|
|
||||||
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
|
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
|
||||||
|
|
||||||
def remove_markdown(text, *, ignore_links=True):
|
|
||||||
|
def remove_markdown(text: str, *, ignore_links: bool = True) -> str:
|
||||||
"""A helper function that removes markdown characters.
|
"""A helper function that removes markdown characters.
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
@@ -567,7 +706,8 @@ def remove_markdown(text, *, ignore_links=True):
|
|||||||
regex = f'(?:{_URL_REGEX}|{regex})'
|
regex = f'(?:{_URL_REGEX}|{regex})'
|
||||||
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
return re.sub(regex, replacement, text, 0, re.MULTILINE)
|
||||||
|
|
||||||
def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
|
||||||
|
def escape_markdown(text: str, *, as_needed: bool = False, ignore_links: bool = True) -> str:
|
||||||
r"""A helper function that escapes Discord's markdown.
|
r"""A helper function that escapes Discord's markdown.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -593,6 +733,7 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not as_needed:
|
if not as_needed:
|
||||||
|
|
||||||
def replacement(match):
|
def replacement(match):
|
||||||
groupdict = match.groupdict()
|
groupdict = match.groupdict()
|
||||||
is_url = groupdict.get('url')
|
is_url = groupdict.get('url')
|
||||||
@@ -608,7 +749,8 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
|
|||||||
text = re.sub(r'\\', r'\\\\', text)
|
text = re.sub(r'\\', r'\\\\', text)
|
||||||
return _MARKDOWN_ESCAPE_REGEX.sub(r'\\\1', text)
|
return _MARKDOWN_ESCAPE_REGEX.sub(r'\\\1', text)
|
||||||
|
|
||||||
def escape_mentions(text):
|
|
||||||
|
def escape_mentions(text: str) -> str:
|
||||||
"""A helper function that escapes everyone, here, role, and user mentions.
|
"""A helper function that escapes everyone, here, role, and user mentions.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -632,3 +774,165 @@ def escape_mentions(text):
|
|||||||
The text with the mentions removed.
|
The text with the mentions removed.
|
||||||
"""
|
"""
|
||||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,20})', '@\u200b\\1', text)
|
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,20})', '@\u200b\\1', text)
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk(iterator: Iterator[T], max_size: int) -> Iterator[List[T]]:
|
||||||
|
ret = []
|
||||||
|
n = 0
|
||||||
|
for item in iterator:
|
||||||
|
ret.append(item)
|
||||||
|
n += 1
|
||||||
|
if n == max_size:
|
||||||
|
yield ret
|
||||||
|
ret = []
|
||||||
|
n = 0
|
||||||
|
if ret:
|
||||||
|
yield ret
|
||||||
|
|
||||||
|
|
||||||
|
async def _achunk(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[List[T]]:
|
||||||
|
ret = []
|
||||||
|
n = 0
|
||||||
|
async for item in iterator:
|
||||||
|
ret.append(item)
|
||||||
|
n += 1
|
||||||
|
if n == max_size:
|
||||||
|
yield ret
|
||||||
|
ret = []
|
||||||
|
n = 0
|
||||||
|
if ret:
|
||||||
|
yield ret
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def as_chunks(iterator: Iterator[T], max_size: int) -> Iterator[List[T]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def as_chunks(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[List[T]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]:
|
||||||
|
"""A helper function that collects an iterator into chunks of a given size.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
iterator: Union[:class:`collections.abc.Iterator`, :class:`collections.abc.AsyncIterator`]
|
||||||
|
The iterator to chunk, can be sync or async.
|
||||||
|
max_size: :class:`int`
|
||||||
|
The maximum chunk size.
|
||||||
|
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The last chunk collected may not be as large as ``max_size``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Union[:class:`Iterator`, :class:`AsyncIterator`]
|
||||||
|
A new iterator which yields chunks of a given size.
|
||||||
|
"""
|
||||||
|
if max_size <= 0:
|
||||||
|
raise ValueError('Chunk sizes must be greater than 0.')
|
||||||
|
|
||||||
|
if isinstance(iterator, AsyncIterator):
|
||||||
|
return _achunk(iterator, max_size)
|
||||||
|
return _chunk(iterator, max_size)
|
||||||
|
|
||||||
|
|
||||||
|
PY_310 = sys.version_info >= (3, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
|
||||||
|
params = []
|
||||||
|
literal_cls = type(Literal[0])
|
||||||
|
for p in parameters:
|
||||||
|
if isinstance(p, literal_cls):
|
||||||
|
params.extend(p.__args__)
|
||||||
|
else:
|
||||||
|
params.append(p)
|
||||||
|
return tuple(params)
|
||||||
|
|
||||||
|
|
||||||
|
def normalise_optional_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
|
||||||
|
none_cls = type(None)
|
||||||
|
return tuple(p for p in parameters if p is not none_cls) + (none_cls,)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_annotation(
|
||||||
|
tp: Any,
|
||||||
|
globals: Dict[str, Any],
|
||||||
|
locals: Dict[str, Any],
|
||||||
|
cache: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
implicit_str: bool = True,
|
||||||
|
):
|
||||||
|
if isinstance(tp, ForwardRef):
|
||||||
|
tp = tp.__forward_arg__
|
||||||
|
# ForwardRefs always evaluate their internals
|
||||||
|
implicit_str = True
|
||||||
|
|
||||||
|
if implicit_str and isinstance(tp, str):
|
||||||
|
if tp in cache:
|
||||||
|
return cache[tp]
|
||||||
|
evaluated = eval(tp, globals, locals)
|
||||||
|
cache[tp] = evaluated
|
||||||
|
return evaluate_annotation(evaluated, globals, locals, cache)
|
||||||
|
|
||||||
|
if hasattr(tp, '__args__'):
|
||||||
|
implicit_str = True
|
||||||
|
is_literal = False
|
||||||
|
args = tp.__args__
|
||||||
|
if not hasattr(tp, '__origin__'):
|
||||||
|
if PY_310 and tp.__class__ is types.Union:
|
||||||
|
converted = Union[args] # type: ignore
|
||||||
|
return evaluate_annotation(converted, globals, locals, cache)
|
||||||
|
|
||||||
|
return tp
|
||||||
|
if tp.__origin__ is Union:
|
||||||
|
try:
|
||||||
|
if args.index(type(None)) != len(args) - 1:
|
||||||
|
args = normalise_optional_params(tp.__args__)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if tp.__origin__ is Literal:
|
||||||
|
if not PY_310:
|
||||||
|
args = flatten_literal_params(tp.__args__)
|
||||||
|
implicit_str = False
|
||||||
|
is_literal = True
|
||||||
|
|
||||||
|
evaluated_args = tuple(evaluate_annotation(arg, globals, locals, cache, implicit_str=implicit_str) for arg in args)
|
||||||
|
|
||||||
|
if is_literal and not all(isinstance(x, (str, int, bool, type(None))) for x in evaluated_args):
|
||||||
|
raise TypeError('Literal arguments must be of type str, int, bool, or NoneType.')
|
||||||
|
|
||||||
|
if evaluated_args == args:
|
||||||
|
return tp
|
||||||
|
|
||||||
|
try:
|
||||||
|
return tp.copy_with(evaluated_args)
|
||||||
|
except AttributeError:
|
||||||
|
return tp.__origin__[evaluated_args]
|
||||||
|
|
||||||
|
return tp
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_annotation(
|
||||||
|
annotation: Any,
|
||||||
|
globalns: Dict[str, Any],
|
||||||
|
localns: Optional[Dict[str, Any]],
|
||||||
|
cache: Optional[Dict[str, Any]],
|
||||||
|
) -> Any:
|
||||||
|
if annotation is None:
|
||||||
|
return type(None)
|
||||||
|
if isinstance(annotation, str):
|
||||||
|
annotation = ForwardRef(annotation)
|
||||||
|
|
||||||
|
locals = globalns if localns is None else localns
|
||||||
|
if cache is None:
|
||||||
|
cache = {}
|
||||||
|
return evaluate_annotation(annotation, globalns, locals, cache)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import socket
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
from . import opus, utils
|
from . import opus, utils
|
||||||
from .backoff import ExponentialBackoff
|
from .backoff import ExponentialBackoff
|
||||||
@@ -71,9 +72,9 @@ class VoiceProtocol:
|
|||||||
This class allows you to implement a protocol to allow for an external
|
This class allows you to implement a protocol to allow for an external
|
||||||
method of sending voice, such as Lavalink_ or a native library implementation.
|
method of sending voice, such as Lavalink_ or a native library implementation.
|
||||||
|
|
||||||
These classes are passed to :meth:`abc.Connectable.connect`.
|
These classes are passed to :meth:`abc.Connectable.connect <VoiceChannel.connect>`.
|
||||||
|
|
||||||
.. _Lavalink: https://github.com/Frederikam/Lavalink
|
.. _Lavalink: https://github.com/freyacodes/Lavalink
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
------------
|
------------
|
||||||
@@ -121,7 +122,7 @@ class VoiceProtocol:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def connect(self, *, timeout, reconnect):
|
async def connect(self, *, timeout: float, reconnect: bool):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
An abstract method called when the client initiates the connection request.
|
An abstract method called when the client initiates the connection request.
|
||||||
@@ -144,7 +145,7 @@ class VoiceProtocol:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def disconnect(self, *, force):
|
async def disconnect(self, *, force: bool):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
An abstract method called when the client terminates the connection.
|
An abstract method called when the client terminates the connection.
|
||||||
@@ -328,7 +329,7 @@ class VoiceClient(VoiceProtocol):
|
|||||||
self._connected.set()
|
self._connected.set()
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
async def connect(self, *, reconnect, timeout):
|
async def connect(self, *, reconnect: bool, timeout: bool):
|
||||||
log.info('Connecting to voice...')
|
log.info('Connecting to voice...')
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
@@ -451,7 +452,7 @@ class VoiceClient(VoiceProtocol):
|
|||||||
log.warning('Could not connect to voice... Retrying...')
|
log.warning('Could not connect to voice... Retrying...')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
async def disconnect(self, *, force=False):
|
async def disconnect(self, *, force: bool = False):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Disconnects this voice client from voice.
|
Disconnects this voice client from voice.
|
||||||
@@ -525,7 +526,7 @@ class VoiceClient(VoiceProtocol):
|
|||||||
|
|
||||||
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]
|
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]
|
||||||
|
|
||||||
def play(self, source, *, after=None):
|
def play(self, source: AudioSource, *, after: Callable[[Exception], Any]=None):
|
||||||
"""Plays an :class:`AudioSource`.
|
"""Plays an :class:`AudioSource`.
|
||||||
|
|
||||||
The finalizer, ``after`` is called after the source has been exhausted
|
The finalizer, ``after`` is called after the source has been exhausted
|
||||||
|
|||||||
1216
discord/webhook.py
1216
discord/webhook.py
File diff suppressed because it is too large
Load Diff
13
discord/webhook/__init__.py
Normal file
13
discord/webhook/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
discord.webhook
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Webhook support
|
||||||
|
|
||||||
|
:copyright: (c) 2015-present Rapptz
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .async_ import *
|
||||||
|
from .sync import *
|
||||||
1523
discord/webhook/async_.py
Normal file
1523
discord/webhook/async_.py
Normal file
File diff suppressed because it is too large
Load Diff
1038
discord/webhook/sync.py
Normal file
1038
discord/webhook/sync.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,24 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, List, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from .utils import snowflake_time, _get_as_snowflake, resolve_invite
|
from .utils import snowflake_time, _get_as_snowflake, resolve_invite
|
||||||
from .user import BaseUser
|
from .user import BaseUser
|
||||||
from .activity import create_activity
|
from .activity import Activity, BaseActivity, Spotify, create_activity
|
||||||
from .invite import Invite
|
from .invite import Invite
|
||||||
from .enums import Status, try_enum
|
from .enums import Status, try_enum
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime
|
||||||
|
from .state import ConnectionState
|
||||||
|
from .types.widget import (
|
||||||
|
WidgetMember as WidgetMemberPayload,
|
||||||
|
Widget as WidgetPayload,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'WidgetChannel',
|
'WidgetChannel',
|
||||||
'WidgetMember',
|
'WidgetMember',
|
||||||
@@ -66,25 +78,24 @@ class WidgetChannel:
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('id', 'name', 'position')
|
__slots__ = ('id', 'name', 'position')
|
||||||
|
|
||||||
|
def __init__(self, id: int, name: str, position: int) -> None:
|
||||||
|
self.id: int = id
|
||||||
|
self.name: str = name
|
||||||
|
self.position: int = position
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __str__(self) -> str:
|
||||||
self.id = kwargs.pop('id')
|
|
||||||
self.name = kwargs.pop('name')
|
|
||||||
self.position = kwargs.pop('position')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<WidgetChannel id={0.id} name={0.name!r} position={0.position!r}>'.format(self)
|
return f'<WidgetChannel id={self.id} name={self.name!r} position={self.position!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mention(self):
|
def mention(self) -> str:
|
||||||
""":class:`str`: The string that allows you to mention the channel."""
|
""":class:`str`: The string that allows you to mention the channel."""
|
||||||
return f'<#{self.id}>'
|
return f'<#{self.id}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@@ -133,32 +144,49 @@ class WidgetMember(BaseUser):
|
|||||||
Whether the member is currently muted.
|
Whether the member is currently muted.
|
||||||
suppress: Optional[:class:`bool`]
|
suppress: Optional[:class:`bool`]
|
||||||
Whether the member is currently being suppressed.
|
Whether the member is currently being suppressed.
|
||||||
connected_channel: Optional[:class:`VoiceChannel`]
|
connected_channel: Optional[:class:`WidgetChannel`]
|
||||||
Which channel the member is connected to.
|
Which channel the member is connected to.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('name', 'status', 'nick', 'avatar', 'discriminator',
|
__slots__ = ('name', 'status', 'nick', 'avatar', 'discriminator',
|
||||||
'id', 'bot', 'activity', 'deafened', 'suppress', 'muted',
|
'id', 'bot', 'activity', 'deafened', 'suppress', 'muted',
|
||||||
'connected_channel')
|
'connected_channel')
|
||||||
|
|
||||||
def __init__(self, *, state, data, connected_channel=None):
|
if TYPE_CHECKING:
|
||||||
|
activity: Optional[Union[BaseActivity, Spotify]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
state: ConnectionState,
|
||||||
|
data: WidgetMemberPayload,
|
||||||
|
connected_channel: Optional[WidgetChannel] = None
|
||||||
|
) -> None:
|
||||||
super().__init__(state=state, data=data)
|
super().__init__(state=state, data=data)
|
||||||
self.nick = data.get('nick')
|
self.nick: Optional[str] = data.get('nick')
|
||||||
self.status = try_enum(Status, data.get('status'))
|
self.status: Status = try_enum(Status, data.get('status'))
|
||||||
self.deafened = data.get('deaf', False) or data.get('self_deaf', False)
|
self.deafened: Optional[bool] = data.get('deaf', False) or data.get('self_deaf', False)
|
||||||
self.muted = data.get('mute', False) or data.get('self_mute', False)
|
self.muted: Optional[bool] = data.get('mute', False) or data.get('self_mute', False)
|
||||||
self.suppress = data.get('suppress', False)
|
self.suppress: Optional[bool] = data.get('suppress', False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
game = data['game']
|
game = data['game']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.activity = None
|
activity = None
|
||||||
else:
|
else:
|
||||||
self.activity = create_activity(game)
|
activity = create_activity(game)
|
||||||
|
|
||||||
self.connected_channel = connected_channel
|
self.activity: Optional[Union[BaseActivity, Spotify]] = activity
|
||||||
|
|
||||||
|
self.connected_channel: Optional[WidgetChannel] = connected_channel
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<WidgetMember name={self.name!r} discriminator={self.discriminator!r}"
|
||||||
|
f" bot={self.bot} nick={self.nick!r}>"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self) -> str:
|
||||||
""":class:`str`: Returns the member's display name."""
|
""":class:`str`: Returns the member's display name."""
|
||||||
return self.nick or self.name
|
return self.nick or self.name
|
||||||
|
|
||||||
@@ -185,9 +213,9 @@ class Widget:
|
|||||||
The guild's ID.
|
The guild's ID.
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
The guild's name.
|
The guild's name.
|
||||||
channels: Optional[List[:class:`WidgetChannel`]]
|
channels: List[:class:`WidgetChannel`]
|
||||||
The accessible voice channels in the guild.
|
The accessible voice channels in the guild.
|
||||||
members: Optional[List[:class:`Member`]]
|
members: List[:class:`Member`]
|
||||||
The online members in the server. Offline members
|
The online members in the server. Offline members
|
||||||
do not appear in the widget.
|
do not appear in the widget.
|
||||||
|
|
||||||
@@ -201,56 +229,58 @@ class Widget:
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('_state', 'channels', '_invite', 'id', 'members', 'name')
|
__slots__ = ('_state', 'channels', '_invite', 'id', 'members', 'name')
|
||||||
|
|
||||||
def __init__(self, *, state, data):
|
def __init__(self, *, state: ConnectionState, data: WidgetPayload) -> None:
|
||||||
self._state = state
|
self._state = state
|
||||||
self._invite = data['instant_invite']
|
self._invite = data['instant_invite']
|
||||||
self.name = data['name']
|
self.name: str = data['name']
|
||||||
self.id = int(data['id'])
|
self.id: int = int(data['id'])
|
||||||
|
|
||||||
self.channels = []
|
self.channels: List[WidgetChannel] = []
|
||||||
for channel in data.get('channels', []):
|
for channel in data.get('channels', []):
|
||||||
_id = int(channel['id'])
|
_id = int(channel['id'])
|
||||||
self.channels.append(WidgetChannel(id=_id, name=channel['name'], position=channel['position']))
|
self.channels.append(WidgetChannel(id=_id, name=channel['name'], position=channel['position']))
|
||||||
|
|
||||||
self.members = []
|
self.members: List[WidgetMember] = []
|
||||||
channels = {channel.id: channel for channel in self.channels}
|
channels = {channel.id: channel for channel in self.channels}
|
||||||
for member in data.get('members', []):
|
for member in data.get('members', []):
|
||||||
connected_channel = _get_as_snowflake(member, 'channel_id')
|
connected_channel = _get_as_snowflake(member, 'channel_id')
|
||||||
if connected_channel in channels:
|
if connected_channel in channels:
|
||||||
connected_channel = channels[connected_channel]
|
connected_channel = channels[connected_channel] # type: ignore
|
||||||
elif connected_channel:
|
elif connected_channel:
|
||||||
connected_channel = WidgetChannel(id=connected_channel, name='', position=0)
|
connected_channel = WidgetChannel(id=connected_channel, name='', position=0)
|
||||||
|
|
||||||
self.members.append(WidgetMember(state=self._state, data=member, connected_channel=connected_channel))
|
self.members.append(WidgetMember(state=self._state, data=member, connected_channel=connected_channel)) # type: ignore
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.json_url
|
return self.json_url
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: Any) -> bool:
|
||||||
return self.id == other.id
|
if isinstance(other, Widget):
|
||||||
|
return self.id == other.id
|
||||||
|
return False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Widget id={0.id} name={0.name!r} invite_url={0.invite_url!r}>'.format(self)
|
return f'<Widget id={self.id} name={self.name!r} invite_url={self.invite_url!r}>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at(self):
|
def created_at(self) -> datetime.datetime:
|
||||||
""":class:`datetime.datetime`: Returns the member's creation time in UTC."""
|
""":class:`datetime.datetime`: Returns the member's creation time in UTC."""
|
||||||
return snowflake_time(self.id)
|
return snowflake_time(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json_url(self):
|
def json_url(self) -> str:
|
||||||
""":class:`str`: The JSON URL of the widget."""
|
""":class:`str`: The JSON URL of the widget."""
|
||||||
return f"https://discord.com/api/guilds/{self.id}/widget.json"
|
return f"https://discord.com/api/guilds/{self.id}/widget.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invite_url(self):
|
def invite_url(self) -> str:
|
||||||
"""Optional[:class:`str`]: The invite URL for the guild, if available."""
|
"""Optional[:class:`str`]: The invite URL for the guild, if available."""
|
||||||
return self._invite
|
return self._invite
|
||||||
|
|
||||||
async def fetch_invite(self, *, with_counts=True):
|
async def fetch_invite(self, *, with_counts: bool = True) -> Invite:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Retrieves an :class:`Invite` from a invite URL or ID.
|
Retrieves an :class:`Invite` from the widget's invite URL.
|
||||||
This is the same as :meth:`Client.fetch_invite`; the invite
|
This is the same as :meth:`Client.fetch_invite`; the invite
|
||||||
code is abstracted away.
|
code is abstracted away.
|
||||||
|
|
||||||
@@ -264,9 +294,8 @@ class Widget:
|
|||||||
Returns
|
Returns
|
||||||
--------
|
--------
|
||||||
:class:`Invite`
|
:class:`Invite`
|
||||||
The invite from the URL/ID.
|
The invite from the widget's invite URL.
|
||||||
"""
|
"""
|
||||||
if self._invite:
|
invite_id = resolve_invite(self._invite)
|
||||||
invite_id = resolve_invite(self._invite)
|
data = await self._state.http.get_invite(invite_id, with_counts=with_counts)
|
||||||
data = await self._state.http.get_invite(invite_id, with_counts=with_counts)
|
return Invite.from_incomplete(state=self._state, data=data)
|
||||||
return Invite.from_incomplete(state=self._state, data=data)
|
|
||||||
|
|||||||
8
docs/_static/style.css
vendored
8
docs/_static/style.css
vendored
@@ -894,11 +894,15 @@ dl.field-list {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* internal references are forced to bold for some reason */
|
/* cross-references are forced to bold for some reason */
|
||||||
a.reference.internal > strong {
|
a.reference > strong {
|
||||||
font-weight: unset;
|
font-weight: unset;
|
||||||
font-family: var(--monospace-font-family);
|
font-family: var(--monospace-font-family);
|
||||||
}
|
}
|
||||||
|
a.reference.pep > strong,
|
||||||
|
a.reference.rfc > strong {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
/* exception hierarchy */
|
/* exception hierarchy */
|
||||||
|
|
||||||
|
|||||||
776
docs/api.rst
776
docs/api.rst
File diff suppressed because it is too large
Load Diff
10
docs/conf.py
10
docs/conf.py
@@ -40,6 +40,7 @@ extensions = [
|
|||||||
'exception_hierarchy',
|
'exception_hierarchy',
|
||||||
'attributetable',
|
'attributetable',
|
||||||
'resourcelinks',
|
'resourcelinks',
|
||||||
|
'nitpick_file_ignorer',
|
||||||
]
|
]
|
||||||
|
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
@@ -53,7 +54,7 @@ extlinks = {
|
|||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'py': ('https://docs.python.org/3', None),
|
'py': ('https://docs.python.org/3', None),
|
||||||
'aio': ('https://docs.aiohttp.org/en/stable/', None),
|
'aio': ('https://docs.aiohttp.org/en/stable/', None),
|
||||||
'req': ('http://docs.python-requests.org/en/latest/', 'requests.inv')
|
'req': ('https://docs.python-requests.org/en/latest/', None)
|
||||||
}
|
}
|
||||||
|
|
||||||
rst_prolog = """
|
rst_prolog = """
|
||||||
@@ -140,6 +141,13 @@ pygments_style = 'friendly'
|
|||||||
#keep_warnings = False
|
#keep_warnings = False
|
||||||
|
|
||||||
|
|
||||||
|
# Nitpicky mode options
|
||||||
|
nitpick_ignore_files = [
|
||||||
|
"migrating_to_async",
|
||||||
|
"migrating",
|
||||||
|
"whats_new",
|
||||||
|
]
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
html_experimental_html5_writer = True
|
html_experimental_html5_writer = True
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
:orphan:
|
||||||
|
|
||||||
.. _discord-intro:
|
.. _discord-intro:
|
||||||
|
|
||||||
Creating a Bot Account
|
Creating a Bot Account
|
||||||
@@ -40,7 +42,7 @@ Creating a Bot account is a pretty straightforward process.
|
|||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
It should be worth noting that this token is essentially your bot's
|
It should be worth noting that this token is essentially your bot's
|
||||||
password. You should **never** share this to someone else. In doing so,
|
password. You should **never** share this with someone else. In doing so,
|
||||||
someone can log in to your bot and do malicious things, such as leaving
|
someone can log in to your bot and do malicious things, such as leaving
|
||||||
servers, ban all members inside a server, or pinging everyone maliciously.
|
servers, ban all members inside a server, or pinging everyone maliciously.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Event Reference
|
|||||||
These events function similar to :ref:`the regular events <discord-api-events>`, except they
|
These events function similar to :ref:`the regular events <discord-api-events>`, except they
|
||||||
are custom to the command extension module.
|
are custom to the command extension module.
|
||||||
|
|
||||||
.. function:: on_command_error(ctx, error)
|
.. function:: discord.ext.commands.on_command_error(ctx, error)
|
||||||
|
|
||||||
An error handler that is called when an error is raised
|
An error handler that is called when an error is raised
|
||||||
inside a command either through user input error, check
|
inside a command either through user input error, check
|
||||||
@@ -55,7 +55,7 @@ are custom to the command extension module.
|
|||||||
:param error: The error that was raised.
|
:param error: The error that was raised.
|
||||||
:type error: :class:`.CommandError` derived
|
:type error: :class:`.CommandError` derived
|
||||||
|
|
||||||
.. function:: on_command(ctx)
|
.. function:: discord.ext.commands.on_command(ctx)
|
||||||
|
|
||||||
An event that is called when a command is found and is about to be invoked.
|
An event that is called when a command is found and is about to be invoked.
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ are custom to the command extension module.
|
|||||||
:param ctx: The invocation context.
|
:param ctx: The invocation context.
|
||||||
:type ctx: :class:`.Context`
|
:type ctx: :class:`.Context`
|
||||||
|
|
||||||
.. function:: on_command_completion(ctx)
|
.. function:: discord.ext.commands.on_command_completion(ctx)
|
||||||
|
|
||||||
An event that is called when a command has completed its invocation.
|
An event that is called when a command has completed its invocation.
|
||||||
|
|
||||||
@@ -176,7 +176,8 @@ Paginator
|
|||||||
Enums
|
Enums
|
||||||
------
|
------
|
||||||
|
|
||||||
.. class:: discord.ext.commands.BucketType
|
.. class:: BucketType
|
||||||
|
:module: discord.ext.commands
|
||||||
|
|
||||||
Specifies a type of bucket for, e.g. a cooldown.
|
Specifies a type of bucket for, e.g. a cooldown.
|
||||||
|
|
||||||
@@ -272,6 +273,9 @@ Converters
|
|||||||
.. autoclass:: discord.ext.commands.Converter
|
.. autoclass:: discord.ext.commands.Converter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: discord.ext.commands.ObjectConverter
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: discord.ext.commands.MemberConverter
|
.. autoclass:: discord.ext.commands.MemberConverter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -284,6 +288,9 @@ Converters
|
|||||||
.. autoclass:: discord.ext.commands.PartialMessageConverter
|
.. autoclass:: discord.ext.commands.PartialMessageConverter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: discord.ext.commands.GuildChannelConverter
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: discord.ext.commands.TextChannelConverter
|
.. autoclass:: discord.ext.commands.TextChannelConverter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -293,6 +300,9 @@ Converters
|
|||||||
.. autoclass:: discord.ext.commands.StoreChannelConverter
|
.. autoclass:: discord.ext.commands.StoreChannelConverter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: discord.ext.commands.StageChannelConverter
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: discord.ext.commands.CategoryChannelConverter
|
.. autoclass:: discord.ext.commands.CategoryChannelConverter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -320,27 +330,20 @@ Converters
|
|||||||
.. autoclass:: discord.ext.commands.clean_content
|
.. autoclass:: discord.ext.commands.clean_content
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. data:: ext.commands.Greedy
|
.. autoclass:: discord.ext.commands.Greedy()
|
||||||
|
|
||||||
A special converter that greedily consumes arguments until it can't.
|
.. autofunction:: discord.ext.commands.run_converters
|
||||||
As a consequence of this behaviour, most input errors are silently discarded,
|
|
||||||
since it is used as an indicator of when to stop parsing.
|
|
||||||
|
|
||||||
When a parser error is met the greedy converter stops converting, undoes the
|
Flag Converter
|
||||||
internal string parsing routine, and continues parsing regularly.
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
For example, in the following code:
|
.. autoclass:: discord.ext.commands.FlagConverter
|
||||||
|
:members:
|
||||||
|
|
||||||
.. code-block:: python3
|
.. autoclass:: discord.ext.commands.Flag()
|
||||||
|
:members:
|
||||||
|
|
||||||
@commands.command()
|
.. autofunction:: discord.ext.commands.flag
|
||||||
async def test(ctx, numbers: Greedy[int], reason: str):
|
|
||||||
await ctx.send("numbers: {}, reason: {}".format(numbers, reason))
|
|
||||||
|
|
||||||
An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with
|
|
||||||
``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\.
|
|
||||||
|
|
||||||
For more information, check :ref:`ext_commands_special_converters`.
|
|
||||||
|
|
||||||
.. _ext_commands_api_errors:
|
.. _ext_commands_api_errors:
|
||||||
|
|
||||||
@@ -374,6 +377,9 @@ Exceptions
|
|||||||
.. autoexception:: discord.ext.commands.BadUnionArgument
|
.. autoexception:: discord.ext.commands.BadUnionArgument
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.BadLiteralArgument
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoexception:: discord.ext.commands.PrivateMessageOnly
|
.. autoexception:: discord.ext.commands.PrivateMessageOnly
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -467,6 +473,21 @@ Exceptions
|
|||||||
.. autoexception:: discord.ext.commands.NSFWChannelRequired
|
.. autoexception:: discord.ext.commands.NSFWChannelRequired
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.FlagError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.BadFlagArgument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.MissingFlagArgument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.TooManyFlags
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.MissingRequiredFlag
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoexception:: discord.ext.commands.ExtensionError
|
.. autoexception:: discord.ext.commands.ExtensionError
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -512,7 +533,13 @@ Exception Hierarchy
|
|||||||
- :exc:`~.commands.EmojiNotFound`
|
- :exc:`~.commands.EmojiNotFound`
|
||||||
- :exc:`~.commands.PartialEmojiConversionFailure`
|
- :exc:`~.commands.PartialEmojiConversionFailure`
|
||||||
- :exc:`~.commands.BadBoolArgument`
|
- :exc:`~.commands.BadBoolArgument`
|
||||||
|
- :exc:`~.commands.FlagError`
|
||||||
|
- :exc:`~.commands.BadFlagArgument`
|
||||||
|
- :exc:`~.commands.MissingFlagArgument`
|
||||||
|
- :exc:`~.commands.TooManyFlags`
|
||||||
|
- :exc:`~.commands.MissingRequiredFlag`
|
||||||
- :exc:`~.commands.BadUnionArgument`
|
- :exc:`~.commands.BadUnionArgument`
|
||||||
|
- :exc:`~.commands.BadLiteralArgument`
|
||||||
- :exc:`~.commands.ArgumentParsingError`
|
- :exc:`~.commands.ArgumentParsingError`
|
||||||
- :exc:`~.commands.UnexpectedQuoteError`
|
- :exc:`~.commands.UnexpectedQuoteError`
|
||||||
- :exc:`~.commands.InvalidEndOfQuotedStringError`
|
- :exc:`~.commands.InvalidEndOfQuotedStringError`
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ This example cog defines a ``Greetings`` category for your commands, with a sing
|
|||||||
async def on_member_join(self, member):
|
async def on_member_join(self, member):
|
||||||
channel = member.guild.system_channel
|
channel = member.guild.system_channel
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
await channel.send('Welcome {0.mention}.'.format(member))
|
await channel.send(f'Welcome {member.mention}.')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def hello(self, ctx, *, member: discord.Member = None):
|
async def hello(self, ctx, *, member: discord.Member = None):
|
||||||
"""Says hello"""
|
"""Says hello"""
|
||||||
member = member or ctx.author
|
member = member or ctx.author
|
||||||
if self._last_member is None or self._last_member.id != member.id:
|
if self._last_member is None or self._last_member.id != member.id:
|
||||||
await ctx.send('Hello {0.name}~'.format(member))
|
await ctx.send(f'Hello {member.name}~')
|
||||||
else:
|
else:
|
||||||
await ctx.send('Hello {0.name}... This feels familiar.'.format(member))
|
await ctx.send(f'Hello {member.name}... This feels familiar.')
|
||||||
self._last_member = member
|
self._last_member = member
|
||||||
|
|
||||||
A couple of technical notes to take into consideration:
|
A couple of technical notes to take into consideration:
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ Since positional arguments are just regular Python arguments, you can have as ma
|
|||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def test(ctx, arg1, arg2):
|
async def test(ctx, arg1, arg2):
|
||||||
await ctx.send('You passed {} and {}'.format(arg1, arg2))
|
await ctx.send(f'You passed {arg1} and {arg2}')
|
||||||
|
|
||||||
Variable
|
Variable
|
||||||
++++++++++
|
++++++++++
|
||||||
@@ -111,7 +111,8 @@ similar to how variable list parameters are done in Python:
|
|||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def test(ctx, *args):
|
async def test(ctx, *args):
|
||||||
await ctx.send('{} arguments: {}'.format(len(args), ', '.join(args)))
|
arguments = ', '.join(args)
|
||||||
|
await ctx.send(f'{len(args)} arguments: {arguments}')
|
||||||
|
|
||||||
This allows our user to accept either one or many arguments as they please. This works similar to positional arguments,
|
This allows our user to accept either one or many arguments as they please. This works similar to positional arguments,
|
||||||
so multi-word parameters should be quoted.
|
so multi-word parameters should be quoted.
|
||||||
@@ -256,7 +257,7 @@ An example converter:
|
|||||||
class Slapper(commands.Converter):
|
class Slapper(commands.Converter):
|
||||||
async def convert(self, ctx, argument):
|
async def convert(self, ctx, argument):
|
||||||
to_slap = random.choice(ctx.guild.members)
|
to_slap = random.choice(ctx.guild.members)
|
||||||
return '{0.author} slapped {1} because *{2}*'.format(ctx, to_slap, argument)
|
return f'{ctx.author} slapped {to_slap} because *{argument}*'
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def slap(ctx, *, reason: Slapper):
|
async def slap(ctx, *, reason: Slapper):
|
||||||
@@ -327,7 +328,7 @@ For example, a common idiom would be to have a class and a converter for that cl
|
|||||||
else:
|
else:
|
||||||
await ctx.send("Hm you're not so new.")
|
await ctx.send("Hm you're not so new.")
|
||||||
|
|
||||||
This can get tedious, so an inline advanced converter is possible through a ``classmethod`` inside the type:
|
This can get tedious, so an inline advanced converter is possible through a :func:`classmethod` inside the type:
|
||||||
|
|
||||||
.. code-block:: python3
|
.. code-block:: python3
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ For example, to receive a :class:`Member` you can just pass it as a converter:
|
|||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def joined(ctx, *, member: discord.Member):
|
async def joined(ctx, *, member: discord.Member):
|
||||||
await ctx.send('{0} joined on {0.joined_at}'.format(member))
|
await ctx.send(f'{member} joined on {member.joined_at}')
|
||||||
|
|
||||||
When this command is executed, it attempts to convert the string given into a :class:`Member` and then passes it as a
|
When this command is executed, it attempts to convert the string given into a :class:`Member` and then passes it as a
|
||||||
parameter for the function. This works by checking if the string is a mention, an ID, a nickname, a username + discriminator,
|
parameter for the function. This works by checking if the string is a mention, an ID, a nickname, a username + discriminator,
|
||||||
@@ -373,12 +374,15 @@ or just a regular username. The default set of converters have been written to b
|
|||||||
|
|
||||||
A lot of discord models work out of the gate as a parameter:
|
A lot of discord models work out of the gate as a parameter:
|
||||||
|
|
||||||
|
- :class:`Object` (since v2.0)
|
||||||
- :class:`Member`
|
- :class:`Member`
|
||||||
- :class:`User`
|
- :class:`User`
|
||||||
- :class:`Message` (since v1.1)
|
- :class:`Message` (since v1.1)
|
||||||
- :class:`PartialMessage` (since v1.7)
|
- :class:`PartialMessage` (since v1.7)
|
||||||
|
- :class:`abc.GuildChannel` (since 2.0)
|
||||||
- :class:`TextChannel`
|
- :class:`TextChannel`
|
||||||
- :class:`VoiceChannel`
|
- :class:`VoiceChannel`
|
||||||
|
- :class:`StageChannel` (since v1.7)
|
||||||
- :class:`StoreChannel` (since v1.7)
|
- :class:`StoreChannel` (since v1.7)
|
||||||
- :class:`CategoryChannel`
|
- :class:`CategoryChannel`
|
||||||
- :class:`Invite`
|
- :class:`Invite`
|
||||||
@@ -398,6 +402,8 @@ converter is given below:
|
|||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
| Discord Class | Converter |
|
| Discord Class | Converter |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
|
| :class:`Object` | :class:`~ext.commands.ObjectConverter` |
|
||||||
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`Member` | :class:`~ext.commands.MemberConverter` |
|
| :class:`Member` | :class:`~ext.commands.MemberConverter` |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`User` | :class:`~ext.commands.UserConverter` |
|
| :class:`User` | :class:`~ext.commands.UserConverter` |
|
||||||
@@ -406,10 +412,14 @@ converter is given below:
|
|||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`PartialMessage` | :class:`~ext.commands.PartialMessageConverter` |
|
| :class:`PartialMessage` | :class:`~ext.commands.PartialMessageConverter` |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
|
| :class:`.GuildChannel` | :class:`~ext.commands.GuildChannelConverter` |
|
||||||
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` |
|
| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` |
|
| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
|
| :class:`StageChannel` | :class:`~ext.commands.StageChannelConverter` |
|
||||||
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
|
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
|
||||||
+--------------------------+-------------------------------------------------+
|
+--------------------------+-------------------------------------------------+
|
||||||
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
|
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
|
||||||
@@ -489,7 +499,7 @@ Consider the following example:
|
|||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def bottles(ctx, amount: typing.Optional[int] = 99, *, liquid="beer"):
|
async def bottles(ctx, amount: typing.Optional[int] = 99, *, liquid="beer"):
|
||||||
await ctx.send('{} bottles of {} on the wall!'.format(amount, liquid))
|
await ctx.send(f'{amount} bottles of {liquid} on the wall!')
|
||||||
|
|
||||||
|
|
||||||
.. image:: /images/commands/optional1.png
|
.. image:: /images/commands/optional1.png
|
||||||
@@ -501,10 +511,31 @@ resumes handling, which in this case would be to pass it into the ``liquid`` par
|
|||||||
|
|
||||||
This converter only works in regular positional parameters, not variable parameters or keyword-only parameters.
|
This converter only works in regular positional parameters, not variable parameters or keyword-only parameters.
|
||||||
|
|
||||||
|
typing.Literal
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A :data:`typing.Literal` is a special type hint that requires the passed parameter to be equal to one of the listed values
|
||||||
|
after being converted to the same type. For example, given the following:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def shop(ctx, buy_sell: Literal['buy', 'sell'], amount: Literal[1, 2], *, item: str):
|
||||||
|
await ctx.send(f'{buy_sell.capitalize()}ing {amount} {item}(s)!')
|
||||||
|
|
||||||
|
|
||||||
|
The ``buy_sell`` parameter must be either the literal string ``"buy"`` or ``"sell"`` and ``amount`` must convert to the
|
||||||
|
``int`` ``1`` or ``2``. If ``buy_sell`` or ``amount`` don't match any value, then a special error is raised,
|
||||||
|
:exc:`~.ext.commands.BadLiteralArgument`. Any literal values can be mixed and matched within the same :data:`typing.Literal` converter.
|
||||||
|
|
||||||
|
Note that ``typing.Literal[True]`` and ``typing.Literal[False]`` still follow the :class:`bool` converter rules.
|
||||||
|
|
||||||
Greedy
|
Greedy
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
|
|
||||||
The :data:`~ext.commands.Greedy` converter is a generalisation of the :data:`typing.Optional` converter, except applied
|
The :class:`~ext.commands.Greedy` converter is a generalisation of the :data:`typing.Optional` converter, except applied
|
||||||
to a list of arguments. In simple terms, this means that it tries to convert as much as it can until it can't convert
|
to a list of arguments. In simple terms, this means that it tries to convert as much as it can until it can't convert
|
||||||
any further.
|
any further.
|
||||||
|
|
||||||
@@ -515,7 +546,7 @@ Consider the following example:
|
|||||||
@bot.command()
|
@bot.command()
|
||||||
async def slap(ctx, members: commands.Greedy[discord.Member], *, reason='no reason'):
|
async def slap(ctx, members: commands.Greedy[discord.Member], *, reason='no reason'):
|
||||||
slapped = ", ".join(x.name for x in members)
|
slapped = ", ".join(x.name for x in members)
|
||||||
await ctx.send('{} just got slapped for {}'.format(slapped, reason))
|
await ctx.send(f'{slapped} just got slapped for {reason}')
|
||||||
|
|
||||||
When invoked, it allows for any number of members to be passed in:
|
When invoked, it allows for any number of members to be passed in:
|
||||||
|
|
||||||
@@ -525,9 +556,9 @@ The type passed when using this converter depends on the parameter type that it
|
|||||||
|
|
||||||
- Positional parameter types will receive either the default parameter or a :class:`list` of the converted values.
|
- Positional parameter types will receive either the default parameter or a :class:`list` of the converted values.
|
||||||
- Variable parameter types will be a :class:`tuple` as usual.
|
- Variable parameter types will be a :class:`tuple` as usual.
|
||||||
- Keyword-only parameter types will be the same as if :data:`~ext.commands.Greedy` was not passed at all.
|
- Keyword-only parameter types will be the same as if :class:`~ext.commands.Greedy` was not passed at all.
|
||||||
|
|
||||||
:data:`~ext.commands.Greedy` parameters can also be made optional by specifying an optional value.
|
:class:`~ext.commands.Greedy` parameters can also be made optional by specifying an optional value.
|
||||||
|
|
||||||
When mixed with the :data:`typing.Optional` converter you can provide simple and expressive command invocation syntaxes:
|
When mixed with the :data:`typing.Optional` converter you can provide simple and expressive command invocation syntaxes:
|
||||||
|
|
||||||
@@ -554,7 +585,7 @@ This command can be invoked any of the following ways:
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
The usage of :data:`~ext.commands.Greedy` and :data:`typing.Optional` are powerful and useful, however as a
|
The usage of :class:`~ext.commands.Greedy` and :data:`typing.Optional` are powerful and useful, however as a
|
||||||
price, they open you up to some parsing ambiguities that might surprise some people.
|
price, they open you up to some parsing ambiguities that might surprise some people.
|
||||||
|
|
||||||
For example, a signature expecting a :data:`typing.Optional` of a :class:`discord.Member` followed by a
|
For example, a signature expecting a :data:`typing.Optional` of a :class:`discord.Member` followed by a
|
||||||
@@ -564,7 +595,165 @@ This command can be invoked any of the following ways:
|
|||||||
allowed through custom converters or reordering the parameters to minimise clashes.
|
allowed through custom converters or reordering the parameters to minimise clashes.
|
||||||
|
|
||||||
To help aid with some parsing ambiguities, :class:`str`, ``None``, :data:`typing.Optional` and
|
To help aid with some parsing ambiguities, :class:`str`, ``None``, :data:`typing.Optional` and
|
||||||
:data:`~ext.commands.Greedy` are forbidden as parameters for the :data:`~ext.commands.Greedy` converter.
|
:class:`~ext.commands.Greedy` are forbidden as parameters for the :class:`~ext.commands.Greedy` converter.
|
||||||
|
|
||||||
|
.. _ext_commands_flag_converter:
|
||||||
|
|
||||||
|
FlagConverter
|
||||||
|
++++++++++++++
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
A :class:`~ext.commands.FlagConverter` allows the user to specify user-friendly "flags" using :pep:`526` type annotations
|
||||||
|
or a syntax more reminiscent of the :mod:`py:dataclasses` module.
|
||||||
|
|
||||||
|
For example, the following code:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class BanFlags(commands.FlagConverter):
|
||||||
|
member: discord.Member
|
||||||
|
reason: str
|
||||||
|
days: int = 1
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def ban(ctx, *, flags: BanFlags):
|
||||||
|
plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
|
||||||
|
await ctx.send(f'Banned {flags.member} for {flags.reason!r} (deleted {plural} worth of messages)')
|
||||||
|
|
||||||
|
Allows the user to invoke the command using a simple flag-like syntax:
|
||||||
|
|
||||||
|
.. image:: /images/commands/flags1.png
|
||||||
|
|
||||||
|
Flags use a syntax that allows the user to not require quotes when passing in values to the flag. The goal of the
|
||||||
|
flag syntax is to be as user-friendly as possible. This makes flags a good choice for complicated commands that can have
|
||||||
|
multiple knobs to turn or simulating keyword-only parameters in your external command interface. **It is recommended to use
|
||||||
|
keyword-only parameters with the flag converter**. This ensures proper parsing and behaviour with quoting.
|
||||||
|
|
||||||
|
Internally, the :class:`~ext.commands.FlagConverter` class examines the class to find flags. A flag can either be a
|
||||||
|
class variable with a type annotation or a class variable that's been assigned the result of the :func:`~ext.commands.flag`
|
||||||
|
function. These flags are then used to define the interface that your users will use. The annotations correspond to
|
||||||
|
the converters that the flag arguments must adhere to.
|
||||||
|
|
||||||
|
For most use cases, no extra work is required to define flags. However, if customisation is needed to control the flag name
|
||||||
|
or the default value then the :func:`~ext.commands.flag` function can come in handy:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
class BanFlags(commands.FlagConverter):
|
||||||
|
members: List[discord.Member] = commands.flag(name='member', default=lambda ctx: [])
|
||||||
|
|
||||||
|
This tells the parser that the ``members`` attribute is mapped to a flag named ``member`` and that
|
||||||
|
the default value is an empty list. For greater customisability, the default can either be a value or a callable
|
||||||
|
that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine.
|
||||||
|
|
||||||
|
In order to customise the flag syntax we also have a few options that can be passed to the class parameter list:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
# --hello world syntax
|
||||||
|
class PosixLikeFlags(commands.FlagConverter, delimiter=' ', prefix='--'):
|
||||||
|
hello: str
|
||||||
|
|
||||||
|
|
||||||
|
# /make food
|
||||||
|
class WindowsLikeFlags(commands.FlagConverter, prefix='/', delimiter=''):
|
||||||
|
make: str
|
||||||
|
|
||||||
|
# TOPIC: not allowed nsfw: yes Slowmode: 100
|
||||||
|
class Settings(commands.FlagConverter, case_insensitive=True):
|
||||||
|
topic: Optional[str]
|
||||||
|
nsfw: Optional[bool]
|
||||||
|
slowmode: Optional[int]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Despite the similarities in these examples to command like arguments, the syntax and parser is not
|
||||||
|
a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result
|
||||||
|
all flags need a corresponding value.
|
||||||
|
|
||||||
|
The flag converter is similar to regular commands and allows you to use most types of converters
|
||||||
|
(with the exception of :class:`~ext.commands.Greedy`) as the type annotation. Some extra support is added for specific
|
||||||
|
annotations as described below.
|
||||||
|
|
||||||
|
typing.List
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
If a list is given as a flag annotation it tells the parser that the argument can be passed multiple times.
|
||||||
|
|
||||||
|
For example, augmenting the example above:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
from typing import List
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class BanFlags(commands.FlagConverter):
|
||||||
|
members: List[discord.Member] = commands.flag(name='member')
|
||||||
|
reason: str
|
||||||
|
days: int = 1
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def ban(ctx, *, flags: BanFlags):
|
||||||
|
for member in flags.members:
|
||||||
|
await member.ban(reason=flags.reason, delete_message_days=flags.days)
|
||||||
|
|
||||||
|
members = ', '.join(str(member) for member in flags.members)
|
||||||
|
plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
|
||||||
|
await ctx.send(f'Banned {members} for {flags.reason!r} (deleted {plural} worth of messages)')
|
||||||
|
|
||||||
|
This is called by repeatedly specifying the flag:
|
||||||
|
|
||||||
|
.. image:: /images/commands/flags2.png
|
||||||
|
|
||||||
|
typing.Tuple
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Since the above syntax can be a bit repetitive when specifying a flag many times, the :class:`py:tuple` type annotation
|
||||||
|
allows for "greedy-like" semantics using a variadic tuple:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
from typing import Tuple
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class BanFlags(commands.FlagConverter):
|
||||||
|
members: Tuple[discord.Member, ...]
|
||||||
|
reason: str
|
||||||
|
days: int = 1
|
||||||
|
|
||||||
|
This allows the previous ``ban`` command to be called like this:
|
||||||
|
|
||||||
|
.. image:: /images/commands/flags3.png
|
||||||
|
|
||||||
|
The :class:`py:tuple` annotation also allows for parsing of pairs. For example, given the following code:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
# point: 10 11 point: 12 13
|
||||||
|
class Coordinates(commands.FlagConverter):
|
||||||
|
point: Tuple[int, int]
|
||||||
|
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Due to potential parsing ambiguities, the parser expects tuple arguments to be quoted
|
||||||
|
if they require spaces. So if one of the inner types is :class:`str` and the argument requires spaces
|
||||||
|
then quotes should be used to disambiguate it from the other element of the tuple.
|
||||||
|
|
||||||
|
typing.Dict
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A :class:`dict` annotation is functionally equivalent to ``List[Tuple[K, V]]`` except with the return type
|
||||||
|
given as a :class:`dict` rather than a :class:`list`.
|
||||||
|
|
||||||
|
|
||||||
.. _ext_commands_error_handler:
|
.. _ext_commands_error_handler:
|
||||||
|
|
||||||
@@ -575,7 +764,7 @@ When our commands fail to parse we will, by default, receive a noisy error in ``
|
|||||||
that an error has happened and has been silently ignored.
|
that an error has happened and has been silently ignored.
|
||||||
|
|
||||||
In order to handle our errors, we must use something called an error handler. There is a global error handler, called
|
In order to handle our errors, we must use something called an error handler. There is a global error handler, called
|
||||||
:func:`on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is
|
:func:`.on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is
|
||||||
called for every error reached.
|
called for every error reached.
|
||||||
|
|
||||||
Most of the time however, we want to handle an error local to the command itself. Luckily, commands come with local error
|
Most of the time however, we want to handle an error local to the command itself. Luckily, commands come with local error
|
||||||
@@ -586,8 +775,8 @@ handlers that allow us to do just that. First we decorate an error handler funct
|
|||||||
@bot.command()
|
@bot.command()
|
||||||
async def info(ctx, *, member: discord.Member):
|
async def info(ctx, *, member: discord.Member):
|
||||||
"""Tells you some info about the member."""
|
"""Tells you some info about the member."""
|
||||||
fmt = '{0} joined on {0.joined_at} and has {1} roles.'
|
msg = f'{member} joined on {member.joined_at} and has {len(member.roles)} roles.'
|
||||||
await ctx.send(fmt.format(member, len(member.roles)))
|
await ctx.send(msg)
|
||||||
|
|
||||||
@info.error
|
@info.error
|
||||||
async def info_error(ctx, error):
|
async def info_error(ctx, error):
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ An example extension looks like this:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def hello(ctx):
|
async def hello(ctx):
|
||||||
await ctx.send('Hello {0.display_name}.'.format(ctx.author))
|
await ctx.send(f'Hello {ctx.author.display_name}.')
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
bot.add_command(hello)
|
bot.add_command(hello)
|
||||||
|
|
||||||
In this example we define a simple command, and when the extension is loaded this command is added to the bot. Now the final step to this is loading the extension, which we do by calling :meth:`.commands.Bot.load_extension`. To load this extension we call ``bot.load_extension('hello')``.
|
In this example we define a simple command, and when the extension is loaded this command is added to the bot. Now the final step to this is loading the extension, which we do by calling :meth:`.Bot.load_extension`. To load this extension we call ``bot.load_extension('hello')``.
|
||||||
|
|
||||||
.. admonition:: Cogs
|
.. admonition:: Cogs
|
||||||
:class: helpful
|
:class: helpful
|
||||||
@@ -41,7 +41,7 @@ In this example we define a simple command, and when the extension is loaded thi
|
|||||||
Reloading
|
Reloading
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
When you make a change to the extension and want to reload the references, the library comes with a function to do this for you, :meth:`Bot.reload_extension`.
|
When you make a change to the extension and want to reload the references, the library comes with a function to do this for you, :meth:`.Bot.reload_extension`.
|
||||||
|
|
||||||
.. code-block:: python3
|
.. code-block:: python3
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user