Browse Source

Rework captcha handling and type errors

pull/10109/head
dolfies 2 years ago
parent
commit
bb952c7060
  1. 1
      discord/__init__.py
  2. 54
      discord/client.py
  3. 102
      discord/errors.py
  4. 108
      discord/handlers.py
  5. 19
      discord/http.py
  6. 57
      discord/types/error.py
  7. 13
      docs/api.rst

1
discord/__init__.py

@ -45,7 +45,6 @@ from .file import *
from .flags import * from .flags import *
from .guild import * from .guild import *
from .guild_premium import * from .guild_premium import *
from .handlers import *
from .integrations import * from .integrations import *
from .interactions import * from .interactions import *
from .invite import * from .invite import *

54
discord/client.py

@ -30,6 +30,7 @@ import logging
from typing import ( from typing import (
Any, Any,
AsyncIterator, AsyncIterator,
Awaitable,
Callable, Callable,
Collection, Collection,
Coroutine, Coroutine,
@ -78,7 +79,6 @@ from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factor
from .profile import UserProfile from .profile import UserProfile
from .connections import Connection from .connections import Connection
from .team import Team from .team import Team
from .handlers import CaptchaHandler
from .billing import PaymentSource, PremiumUsage from .billing import PaymentSource, PremiumUsage
from .subscriptions import Subscription, SubscriptionItem, SubscriptionInvoice from .subscriptions import Subscription, SubscriptionItem, SubscriptionInvoice
from .payments import Payment from .payments import Payment
@ -222,10 +222,14 @@ class Client:
`aiohttp documentation <https://docs.aiohttp.org/en/stable/client_advanced.html#client-tracing>`_. `aiohttp documentation <https://docs.aiohttp.org/en/stable/client_advanced.html#client-tracing>`_.
.. versionadded:: 2.0 .. versionadded:: 2.0
captcha_handler: Optional[:class:`CaptchaHandler`] captcha_handler: Optional[Callable[[:class:`.CaptchaRequired`, :class:`.Client`], Awaitable[:class:`str`]]
A class that solves captcha challenges. A function that solves captcha challenges.
.. versionadded:: 2.0 .. versionadded:: 2.0
.. versionchanged:: 2.1
Now accepts a coroutine instead of a ``CaptchaHandler``.
max_ratelimit_timeout: Optional[:class:`float`] max_ratelimit_timeout: Optional[:class:`float`]
The maximum number of seconds to wait when a non-global rate limit is encountered. The maximum number of seconds to wait when a non-global rate limit is encountered.
If a request requires sleeping for more than the seconds passed in, then If a request requires sleeping for more than the seconds passed in, then
@ -251,17 +255,16 @@ class Client:
proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None) proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None)
unsync_clock: bool = options.pop('assume_unsync_clock', True) unsync_clock: bool = options.pop('assume_unsync_clock', True)
http_trace: Optional[aiohttp.TraceConfig] = options.pop('http_trace', None) http_trace: Optional[aiohttp.TraceConfig] = options.pop('http_trace', None)
captcha_handler: Optional[CaptchaHandler] = options.pop('captcha_handler', None)
if captcha_handler is not None and not isinstance(captcha_handler, CaptchaHandler):
raise TypeError(f'captcha_handler must derive from CaptchaHandler')
max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None) max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None)
self.captcha_handler: Optional[Callable[[CaptchaRequired, Client], Awaitable[str]]] = options.pop(
'captcha_handler', None
)
self.http: HTTPClient = HTTPClient( self.http: HTTPClient = HTTPClient(
self.loop,
proxy=proxy, proxy=proxy,
proxy_auth=proxy_auth, proxy_auth=proxy_auth,
unsync_clock=unsync_clock, unsync_clock=unsync_clock,
http_trace=http_trace, http_trace=http_trace,
captcha_handler=captcha_handler, captcha=self.handle_captcha,
max_ratelimit_timeout=max_ratelimit_timeout, max_ratelimit_timeout=max_ratelimit_timeout,
locale=lambda: self._connection.locale, locale=lambda: self._connection.locale,
) )
@ -549,6 +552,8 @@ class Client:
def get_experiment(self, experiment: Union[str, int], /) -> Optional[Union[UserExperiment, GuildExperiment]]: def get_experiment(self, experiment: Union[str, int], /) -> Optional[Union[UserExperiment, GuildExperiment]]:
"""Returns a user or guild experiment from the given experiment identifier. """Returns a user or guild experiment from the given experiment identifier.
.. versionadded:: 2.1
Parameters Parameters
----------- -----------
experiment: Union[:class:`str`, :class:`int`] experiment: Union[:class:`str`, :class:`int`]
@ -668,7 +673,7 @@ class Client:
""" """
_log.exception('Ignoring exception in %s', event_method) _log.exception('Ignoring exception in %s', event_method)
async def on_internal_settings_update(self, old_settings: UserSettings, new_settings: UserSettings): async def on_internal_settings_update(self, old_settings: UserSettings, new_settings: UserSettings, /):
if not self._sync_presences: if not self._sync_presences:
return return
@ -712,11 +717,40 @@ class Client:
""" """
pass pass
async def handle_captcha(self, exception: CaptchaRequired, /) -> str:
"""|coro|
Handles a CAPTCHA challenge and returns a solution.
The default implementation tries to use the CAPTCHA handler
passed in the constructor.
.. versionadded:: 2.1
Parameters
------------
exception: :class:`.CaptchaRequired`
The exception that was raised.
Raises
--------
CaptchaRequired
The CAPTCHA challenge could not be solved.
Returns
--------
:class:`str`
The solution to the CAPTCHA challenge.
"""
handler = self.captcha_handler
if handler is None:
raise exception
return await handler(exception, self)
async def _async_setup_hook(self) -> None: async def _async_setup_hook(self) -> None:
# Called whenever the client needs to initialise asyncio objects with a running loop # Called whenever the client needs to initialise asyncio objects with a running loop
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
self.loop = loop self.loop = loop
self.http.loop = loop
self._connection.loop = loop self._connection.loop = loop
await self._connection.async_setup() await self._connection.async_setup()

102
discord/errors.py

@ -23,13 +23,23 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Dict, List, Optional, TYPE_CHECKING, Any, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, Tuple, Union
from .utils import _get_as_snowflake from .utils import _get_as_snowflake
if TYPE_CHECKING: if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientWebSocketResponse from aiohttp import ClientResponse, ClientWebSocketResponse
from requests import Response from requests import Response
from typing_extensions import TypeGuard
from .types.error import (
CaptchaRequired as CaptchaPayload,
CaptchaService,
Error as ErrorPayload,
FormErrors as FormErrorsPayload,
FormErrorWrapper as FormErrorWrapperPayload,
)
_ResponseType = Union[ClientResponse, Response] _ResponseType = Union[ClientResponse, Response]
@ -56,7 +66,7 @@ class DiscordException(Exception):
Ideally speaking, this could be caught to handle any exceptions raised from this library. Ideally speaking, this could be caught to handle any exceptions raised from this library.
""" """
pass __slots__ = ()
class ClientException(DiscordException): class ClientException(DiscordException):
@ -65,7 +75,7 @@ class ClientException(DiscordException):
These are usually for exceptions that happened due to user input. These are usually for exceptions that happened due to user input.
""" """
pass __slots__ = ()
class GatewayNotFound(DiscordException): class GatewayNotFound(DiscordException):
@ -76,25 +86,27 @@ class GatewayNotFound(DiscordException):
super().__init__(message) super().__init__(message)
def _flatten_error_dict(d: Dict[str, Any], key: str = '') -> Dict[str, str]: def _flatten_error_dict(d: FormErrorsPayload, key: str = '', /) -> Dict[str, str]:
def is_wrapper(x: FormErrorsPayload) -> TypeGuard[FormErrorWrapperPayload]:
return '_errors' in x
items: List[Tuple[str, str]] = [] items: List[Tuple[str, str]] = []
if '_errors' in d: if is_wrapper(d) and not key:
items.append(('miscallenous', ' '.join(x.get('message', '') for x in d['_errors']))) items.append(('miscellaneous', ' '.join(x.get('message', '') for x in d['_errors'])))
d.pop('_errors') d.pop('_errors') # type: ignore
for k, v in d.items(): for k, v in d.items():
new_key = key + '.' + k if key else k new_key = key + '.' + k if key else k
if isinstance(v, dict): if isinstance(v, dict):
try: if is_wrapper(v):
_errors: List[Dict[str, Any]] = v['_errors'] _errors = v['_errors']
except KeyError:
items.extend(_flatten_error_dict(v, new_key).items())
else:
items.append((new_key, ' '.join(x.get('message', '') for x in _errors))) items.append((new_key, ' '.join(x.get('message', '') for x in _errors)))
else:
items.extend(_flatten_error_dict(v, new_key).items())
else: else:
items.append((new_key, v)) items.append((new_key, v)) # type: ignore
return dict(items) return dict(items)
@ -127,12 +139,12 @@ class HTTPException(DiscordException):
def __init__(self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]): def __init__(self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]):
self.response: _ResponseType = response self.response: _ResponseType = response
self.status: int = response.status # type: ignore # This attribute is filled by the library even if using requests self.status: int = response.status # type: ignore # This attribute is filled by the library even if using requests
self.code: int self.code: int = 0
self.text: str self.text: str
self.json: Dict[str, Any] self.json: ErrorPayload
self.payment_id: Optional[int] self.payment_id: Optional[int] = None
if isinstance(message, dict): if isinstance(message, dict):
self.json = message self.json = message # type: ignore
self.code = message.get('code', 0) self.code = message.get('code', 0)
base = message.get('message', '') base = message.get('message', '')
errors = message.get('errors') errors = message.get('errors')
@ -145,9 +157,7 @@ class HTTPException(DiscordException):
self.payment_id = _get_as_snowflake(message, 'payment_id') self.payment_id = _get_as_snowflake(message, 'payment_id')
else: else:
self.text = message or '' self.text = message or ''
self.code = 0 self.json = {'code': 0, 'message': message or ''}
self.json = {}
self.payment_id = None
fmt = '{0.status} {0.reason} (error code: {1})' fmt = '{0.status} {0.reason} (error code: {1})'
if len(self.text): if len(self.text):
@ -175,6 +185,8 @@ class RateLimited(DiscordException):
the request. the request.
""" """
__slots__ = ('retry_after',)
def __init__(self, retry_after: float): def __init__(self, retry_after: float):
self.retry_after = retry_after self.retry_after = retry_after
super().__init__(f'Too many requests. Retry in {retry_after:.2f} seconds.') super().__init__(f'Too many requests. Retry in {retry_after:.2f} seconds.')
@ -186,7 +198,7 @@ class Forbidden(HTTPException):
Subclass of :exc:`HTTPException` Subclass of :exc:`HTTPException`
""" """
pass __slots__ = ()
class NotFound(HTTPException): class NotFound(HTTPException):
@ -195,7 +207,7 @@ class NotFound(HTTPException):
Subclass of :exc:`HTTPException` Subclass of :exc:`HTTPException`
""" """
pass __slots__ = ()
class DiscordServerError(HTTPException): class DiscordServerError(HTTPException):
@ -206,20 +218,52 @@ class DiscordServerError(HTTPException):
.. versionadded:: 1.5 .. versionadded:: 1.5
""" """
pass __slots__ = ()
class CaptchaRequired(HTTPException): class CaptchaRequired(HTTPException):
"""Exception that's raised when a captcha is required and isn't handled. """Exception that's raised when a CAPTCHA is required and isn't handled.
Subclass of :exc:`HTTPException`. Subclass of :exc:`HTTPException`.
.. versionadded:: 2.0 .. versionadded:: 2.0
Attributes
------------
errors: List[:class:`str`]
The CAPTCHA service errors.
.. versionadded:: 2.1
service: :class:`str`
The CAPTCHA service to use. Usually ``hcaptcha``.
.. versionadded:: 2.1
sitekey: :class:`str`
The CAPTCHA sitekey to use.
.. versionadded:: 2.1
rqdata: Optional[:class:`str`]
The enterprise hCaptcha request data.
.. versionadded:: 2.1
rqtoken: Optional[:class:`str`]
The enterprise hCaptcha request token.
.. versionadded:: 2.1
""" """
def __init__(self, response: _ResponseType, message: Dict[str, Any]): RECAPTCHA_SITEKEY: Final[str] = '6Lef5iQTAAAAAKeIvIY-DeexoO3gj7ryl9rLMEnn'
__slots__ = ('errors', 'service', 'sitekey')
def __init__(self, response: _ResponseType, message: CaptchaPayload):
super().__init__(response, {'code': -1, 'message': 'Captcha required'}) super().__init__(response, {'code': -1, 'message': 'Captcha required'})
self.json = message self.json: CaptchaPayload = message
self.errors: List[str] = message['captcha_key']
self.service: CaptchaService = message.get('captcha_service', 'hcaptcha')
self.sitekey: str = message.get('captcha_sitekey') or self.RECAPTCHA_SITEKEY
self.rqdata: Optional[str] = message.get('captcha_rqdata')
self.rqtoken: Optional[str] = message.get('captcha_rqtoken')
class InvalidData(ClientException): class InvalidData(ClientException):
@ -227,7 +271,7 @@ class InvalidData(ClientException):
or invalid data from Discord. or invalid data from Discord.
""" """
pass __slots__ = ()
class LoginFailure(ClientException): class LoginFailure(ClientException):
@ -236,7 +280,7 @@ class LoginFailure(ClientException):
failure. failure.
""" """
pass __slots__ = ()
AuthFailure = LoginFailure AuthFailure = LoginFailure
@ -254,6 +298,8 @@ class ConnectionClosed(ClientException):
The reason provided for the closure. The reason provided for the closure.
""" """
__slots__ = ('code', 'reason')
def __init__(self, socket: ClientWebSocketResponse, *, code: Optional[int] = None): def __init__(self, socket: ClientWebSocketResponse, *, code: Optional[int] = None):
# This exception is just the same exception except # This exception is just the same exception except
# reconfigured to subclass ClientException for users # reconfigured to subclass ClientException for users

108
discord/handlers.py

@ -1,108 +0,0 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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, Dict, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp import BasicAuth
# fmt: off
__all__ = (
'CaptchaHandler',
)
# fmt: on
class CaptchaHandler:
"""A class that represents a captcha handler.
This class allows you to implement a protocol to solve captchas required by Discord.
This is an abstract class. The library provides no concrete implementation.
These classes are passed to :class:`Client`.
"""
async def startup(self):
"""|coro|
An abstract method that is called by the library at startup.
This is meant to provide an async startup method for the handler.
This isn't guaranteed to be run once.
The default implementation does nothing.
"""
pass
async def prefetch_token(self, proxy: Optional[str], proxy_auth: Optional[BasicAuth], /) -> None:
"""|coro|
An abstract method that is called a bit before a captcha token is required.
Not guaranteed to be called.
It's meant to signal the handler to begin preparing for the fetching of a token, if applicable.
Keep in mind that Discord has multiple captcha sitekeys.
The default implementation does nothing.
Parameters
----------
proxy: Optional[:class:`str`]
The current proxy of the client.
proxy_auth: Optional[:class:`aiohttp.BasicAuth`]
The proxy's auth.
"""
pass
async def fetch_token(
self,
data: Dict[str, Any],
proxy: Optional[str],
proxy_auth: Optional[BasicAuth],
/,
) -> str:
"""|coro|
An abstract method that is called to fetch a captcha token.
If there is no token available, it should wait until one is
generated before returning.
Parameters
------------
data: Dict[:class:`str`, :class:`Any`]
The raw error from Discord containing the captcha info.
proxy: Optional[:class:`str`]
The current proxy of the client.
proxy_auth: Optional[:class:`aiohttp.BasicAuth`]
The proxy's auth.
Returns
--------
:class:`str`
A captcha token.
"""
raise NotImplementedError

19
discord/http.py

@ -75,7 +75,6 @@ if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel
from .handlers import CaptchaHandler
from .threads import Thread from .threads import Thread
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .message import Attachment, Message from .message import Attachment, Message
@ -568,18 +567,16 @@ class HTTPClient:
def __init__( def __init__(
self, self,
loop: asyncio.AbstractEventLoop,
connector: Optional[aiohttp.BaseConnector] = None, connector: Optional[aiohttp.BaseConnector] = None,
*, *,
proxy: Optional[str] = None, proxy: Optional[str] = None,
proxy_auth: Optional[aiohttp.BasicAuth] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None,
unsync_clock: bool = True, unsync_clock: bool = True,
http_trace: Optional[aiohttp.TraceConfig] = None, http_trace: Optional[aiohttp.TraceConfig] = None,
captcha_handler: Optional[CaptchaHandler] = None, captcha: Optional[Callable[[CaptchaRequired], Coroutine[Any, Any, str]]] = None,
max_ratelimit_timeout: Optional[float] = None, max_ratelimit_timeout: Optional[float] = None,
locale: Callable[[], str] = lambda: 'en-US', locale: Callable[[], str] = lambda: 'en-US',
) -> None: ) -> None:
self.loop: asyncio.AbstractEventLoop = loop
self.connector: aiohttp.BaseConnector = connector or MISSING self.connector: aiohttp.BaseConnector = connector or MISSING
self.__session: aiohttp.ClientSession = MISSING self.__session: aiohttp.ClientSession = MISSING
# Route key -> Bucket hash # Route key -> Bucket hash
@ -598,7 +595,7 @@ class HTTPClient:
self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth
self.http_trace: Optional[aiohttp.TraceConfig] = http_trace self.http_trace: Optional[aiohttp.TraceConfig] = http_trace
self.use_clock: bool = not unsync_clock self.use_clock: bool = not unsync_clock
self.captcha_handler: Optional[CaptchaHandler] = captcha_handler self.captcha_handler: Optional[Callable[[CaptchaRequired], Coroutine[Any, Any, str]]] = captcha
self.max_ratelimit_timeout: Optional[float] = max(30.0, max_ratelimit_timeout) if max_ratelimit_timeout else None self.max_ratelimit_timeout: Optional[float] = max(30.0, max_ratelimit_timeout) if max_ratelimit_timeout else None
self.get_locale: Callable[[], str] = locale self.get_locale: Callable[[], str] = locale
@ -635,9 +632,6 @@ class HTTPClient:
self.super_properties, self.encoded_super_properties = sp, _ = await utils._get_info(session) self.super_properties, self.encoded_super_properties = sp, _ = await utils._get_info(session)
_log.info('Found user agent %s, build number %s.', sp.get('browser_user_agent'), sp.get('client_build_number')) _log.info('Found user agent %s, build number %s.', sp.get('browser_user_agent'), sp.get('client_build_number'))
if self.captcha_handler is not None:
await self.captcha_handler.startup()
self._started = True self._started = True
async def ws_connect(self, url: str, *, compress: int = 0) -> aiohttp.ClientWebSocketResponse: async def ws_connect(self, url: str, *, compress: int = 0) -> aiohttp.ClientWebSocketResponse:
@ -930,7 +924,7 @@ class HTTPClient:
raise DiscordServerError(response, data) raise DiscordServerError(response, data)
else: else:
if isinstance(data, dict) and 'captcha_key' in data: if isinstance(data, dict) and 'captcha_key' in data:
raise CaptchaRequired(response, data) raise CaptchaRequired(response, data) # type: ignore
raise HTTPException(response, data) raise HTTPException(response, data)
# This is handling exceptions from the request # This is handling exceptions from the request
@ -950,10 +944,9 @@ class HTTPClient:
if captcha_handler is None or tries == 4: if captcha_handler is None or tries == 4:
raise raise
else: else:
headers['X-Captcha-Key'] = await captcha_handler.fetch_token(e.json, self.proxy, self.proxy_auth) headers['X-Captcha-Key'] = await captcha_handler(e)
rqtoken = e.json.get('captcha_rqtoken') if e.rqtoken:
if rqtoken: headers['X-Captcha-Rqtoken'] = e.rqtoken
headers['X-Captcha-Rqtoken'] = rqtoken
if response is not None: if response is not None:
# We've run out of retries, raise # We've run out of retries, raise

57
discord/types/error.py

@ -0,0 +1,57 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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 Dict, List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
class FormError(TypedDict):
code: str
message: str
class FormErrorWrapper(TypedDict):
_errors: List[FormError]
FormErrors = Union[FormErrorWrapper, Dict[str, 'FormErrors']]
class Error(TypedDict):
code: int
message: str
errors: NotRequired[FormErrors]
CaptchaService = Literal['hcaptcha', 'recaptcha']
class CaptchaRequired(TypedDict):
captcha_key: List[str]
captcha_service: CaptchaService
captcha_sitekey: Optional[str]
captcha_rqdata: NotRequired[str]
captcha_rqtoken: NotRequired[str]

13
docs/api.rst

@ -45,14 +45,6 @@ Client
.. automethod:: Client.event() .. automethod:: Client.event()
:decorator: :decorator:
CaptchaHandler
~~~~~~~~~~~~~~
.. attributetable:: CaptchaHandler
.. autoclass:: CaptchaHandler
:members:
Voice Related Voice Related
--------------- ---------------
@ -8108,8 +8100,12 @@ The following exceptions are thrown by the library.
:members: :members:
.. autoexception:: Forbidden .. autoexception:: Forbidden
:members:
:inherited-members:
.. autoexception:: NotFound .. autoexception:: NotFound
:members:
:inherited-members:
.. autoexception:: CaptchaRequired .. autoexception:: CaptchaRequired
:members: :members:
@ -8122,6 +8118,7 @@ The following exceptions are thrown by the library.
.. autoexception:: GatewayNotFound .. autoexception:: GatewayNotFound
.. autoexception:: ConnectionClosed .. autoexception:: ConnectionClosed
:members:
.. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusError

Loading…
Cancel
Save